@jsreport/jsreport-core 3.0.1 → 3.1.2-test.2

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.
Files changed (79) hide show
  1. package/LICENSE +166 -166
  2. package/README.md +298 -284
  3. package/index.js +29 -27
  4. package/lib/main/blobStorage/blobStorage.js +52 -47
  5. package/lib/main/blobStorage/inMemoryProvider.js +27 -27
  6. package/lib/main/blobStorage/mainActions.js +24 -24
  7. package/lib/main/createDefaultLoggerFormat.js +17 -17
  8. package/lib/main/defaults.js +14 -14
  9. package/lib/main/extensions/discover.js +20 -20
  10. package/lib/main/extensions/extensionsManager.js +264 -265
  11. package/lib/main/extensions/fileUtils.js +56 -55
  12. package/lib/main/extensions/findVersion.js +49 -53
  13. package/lib/main/extensions/locationCache.js +103 -97
  14. package/lib/main/extensions/sorter.js +10 -10
  15. package/lib/main/extensions/validateMinimalVersion.js +50 -50
  16. package/lib/main/folders/cascadeFolderRemove.js +25 -25
  17. package/lib/main/folders/getEntitiesInFolder.js +53 -53
  18. package/lib/main/folders/index.js +42 -42
  19. package/lib/main/folders/moveBetweenFolders.js +354 -354
  20. package/lib/main/folders/validateDuplicatedName.js +107 -107
  21. package/lib/main/folders/validateReservedName.js +53 -53
  22. package/lib/main/logger.js +244 -244
  23. package/lib/main/migration/resourcesToAssets.js +230 -210
  24. package/lib/main/migration/xlsxTemplatesToAssets.js +128 -118
  25. package/lib/main/monitoring.js +91 -91
  26. package/lib/main/optionsLoad.js +237 -237
  27. package/lib/main/optionsSchema.js +237 -237
  28. package/lib/main/reporter.js +579 -578
  29. package/lib/main/schemaValidator.js +252 -252
  30. package/lib/main/settings.js +154 -154
  31. package/lib/main/store/checkDuplicatedId.js +27 -27
  32. package/lib/main/store/collection.js +329 -329
  33. package/lib/main/store/documentStore.js +469 -469
  34. package/lib/main/store/mainActions.js +28 -28
  35. package/lib/main/store/memoryStoreProvider.js +99 -99
  36. package/lib/main/store/queue.js +48 -48
  37. package/lib/main/store/referenceUtils.js +251 -251
  38. package/lib/main/store/setupValidateId.js +43 -43
  39. package/lib/main/store/setupValidateShortid.js +71 -71
  40. package/lib/main/store/transaction.js +69 -69
  41. package/lib/main/store/typeUtils.js +180 -180
  42. package/lib/main/templates.js +34 -34
  43. package/lib/main/validateEntityName.js +62 -62
  44. package/lib/shared/createError.js +36 -36
  45. package/lib/shared/encryption.js +114 -114
  46. package/lib/shared/folders/index.js +11 -11
  47. package/lib/shared/folders/normalizeEntityPath.js +15 -15
  48. package/lib/shared/folders/resolveEntityFromPath.js +88 -88
  49. package/lib/shared/folders/resolveEntityPath.js +46 -46
  50. package/lib/shared/folders/resolveFolderFromPath.js +38 -38
  51. package/lib/shared/generateRequestId.js +4 -4
  52. package/lib/shared/listenerCollection.js +169 -0
  53. package/lib/shared/normalizeMetaFromLogs.js +30 -30
  54. package/lib/shared/reporter.js +123 -123
  55. package/lib/shared/request.js +64 -64
  56. package/lib/shared/tempFilesHandler.js +81 -81
  57. package/lib/shared/templates.js +82 -82
  58. package/lib/static/helpers.js +33 -33
  59. package/lib/worker/blobStorage.js +34 -34
  60. package/lib/worker/defaultProxyExtend.js +46 -46
  61. package/lib/worker/documentStore.js +49 -49
  62. package/lib/worker/extensionsManager.js +17 -17
  63. package/lib/worker/logger.js +48 -48
  64. package/lib/worker/render/diff.js +138 -138
  65. package/lib/worker/render/executeEngine.js +207 -200
  66. package/lib/worker/render/htmlRecipe.js +10 -10
  67. package/lib/worker/render/moduleHelper.js +43 -43
  68. package/lib/worker/render/noneEngine.js +12 -12
  69. package/lib/worker/render/profiler.js +158 -158
  70. package/lib/worker/render/render.js +205 -209
  71. package/lib/worker/render/resolveReferences.js +60 -60
  72. package/lib/worker/reporter.js +191 -187
  73. package/lib/worker/sandbox/runInSandbox.js +13 -4
  74. package/lib/worker/sandbox/safeSandbox.js +828 -822
  75. package/lib/worker/templates.js +78 -78
  76. package/lib/worker/workerHandler.js +54 -54
  77. package/package.json +92 -92
  78. package/test/blobStorage/common.js +21 -21
  79. package/test/store/common.js +1449 -1449
@@ -1,822 +1,828 @@
1
- const os = require('os')
2
- 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')
12
- const stackTrace = require('stack-trace')
13
- const { codeFrameColumns } = require('@babel/code-frame')
14
-
15
- module.exports = (_sandbox, options = {}) => {
16
- const {
17
- onLog,
18
- formatError,
19
- propertiesConfig = {},
20
- globalModules = [],
21
- allowedModules = [],
22
- requireMap
23
- } = options
24
-
25
- const modulesCache = options.modulesCache != null ? options.modulesCache : Object.create(null)
26
- const _console = {}
27
-
28
- let requirePaths = options.requirePaths || []
29
-
30
- requirePaths = requirePaths.filter((p) => p != null).map((p) => {
31
- if (p.endsWith('/') || p.endsWith('\\')) {
32
- return p.slice(0, -1)
33
- }
34
-
35
- return p
36
- })
37
-
38
- // remove duplicates in paths
39
- requirePaths = requirePaths.filter((v, i) => requirePaths.indexOf(v) === i)
40
-
41
- function addConsoleMethod (consoleMethod, level) {
42
- _console[consoleMethod] = function () {
43
- if (onLog == null) {
44
- return
45
- }
46
-
47
- onLog({
48
- timestamp: new Date().getTime(),
49
- level: level,
50
- message: util.format.apply(util, arguments)
51
- })
52
- }
53
- }
54
-
55
- addConsoleMethod('log', 'debug')
56
- addConsoleMethod('warn', 'warn')
57
- addConsoleMethod('error', 'error')
58
-
59
- const _require = function (moduleName, { context, allowAllModules = false } = {}) {
60
- if (requireMap) {
61
- const mapResult = requireMap(moduleName, { context })
62
-
63
- if (mapResult != null) {
64
- return mapResult
65
- }
66
- }
67
-
68
- if (allowAllModules || allowedModules === '*') {
69
- return doRequire(moduleName, requirePaths, modulesCache)
70
- }
71
-
72
- const m = allowedModules.find(mod => (mod.id || mod) === moduleName)
73
-
74
- if (m) {
75
- return doRequire(m.path || moduleName, requirePaths, modulesCache)
76
- }
77
-
78
- const error = new Error(
79
- `require of "${moduleName}" module has been blocked.`
80
- )
81
-
82
- if (formatError) {
83
- formatError(error, moduleName)
84
- }
85
-
86
- throw error
87
- }
88
-
89
- for (const info of globalModules) {
90
- // it is important to use "doRequire" function here to avoid
91
- // getting hit by the allowed modules restriction
92
- _sandbox[info.globalVariableName] = doRequire(info.module, requirePaths, modulesCache)
93
- }
94
-
95
- const propsConfig = normalizePropertiesConfigInHierarchy(propertiesConfig)
96
- const originalValues = {}
97
- const proxiesInVM = new WeakMap()
98
- const customProxies = new WeakMap()
99
-
100
- // we copy the object based on config to avoid sharing same context
101
- // (with getters/setters) in the rest of request pipeline
102
- const sandbox = copyBasedOnPropertiesConfig(_sandbox, propertiesConfig)
103
-
104
- applyPropertiesConfig(sandbox, propsConfig, {
105
- original: originalValues,
106
- customProxies
107
- })
108
-
109
- Object.assign(sandbox, {
110
- console: _console,
111
- require: (m) => _require(m, { context: _sandbox })
112
- })
113
-
114
- const vm = new VM()
115
-
116
- // NOTE: we wrap the Contextify.object, Decontextify.object methods because those are the
117
- // methods that returns the proxies created by vm2 in the sandbox, we want to have a list of those
118
- // to later use them
119
- const wrapAndSaveProxyResult = (originalFn, thisArg) => {
120
- return (value, ...args) => {
121
- const result = originalFn.call(thisArg, value, ...args)
122
-
123
- if (result != null && result.isVMProxy === true) {
124
- proxiesInVM.set(result, value)
125
- }
126
-
127
- return result
128
- }
129
- }
130
-
131
- vm._internal.Contextify.object = wrapAndSaveProxyResult(vm._internal.Contextify.object, vm._internal.Contextify)
132
- vm._internal.Decontextify.object = wrapAndSaveProxyResult(vm._internal.Decontextify.object, vm._internal.Decontextify)
133
-
134
- for (const name in sandbox) {
135
- vm._internal.Contextify.setGlobal(name, sandbox[name])
136
- }
137
-
138
- // processing top level props because getter/setter descriptors
139
- // for top level properties will only work after VM instantiation
140
- Object.keys(propsConfig).forEach((key) => {
141
- const currentConfig = propsConfig[key]
142
-
143
- if (currentConfig.root && currentConfig.root.sandboxReadOnly) {
144
- readOnlyProp(vm._context, key, [], customProxies, { onlyTopLevel: true })
145
- }
146
- })
147
-
148
- const sourceFilesInfo = new Map()
149
-
150
- return {
151
- sandbox: vm._context,
152
- console: _console,
153
- contextifyValue: (value) => {
154
- return vm._internal.Contextify.value(value)
155
- },
156
- decontextifyValue: (value) => {
157
- return vm._internal.Decontextify.value(value)
158
- },
159
- restore: () => {
160
- return restoreProperties(vm._context, originalValues, proxiesInVM, customProxies)
161
- },
162
- unproxyValue: (value) => {
163
- return getOriginalFromProxy(proxiesInVM, customProxies, value)
164
- },
165
- safeRequire: (modulePath) => _require(modulePath, { context: _sandbox, allowAllModules: true }),
166
- run: async (code, { filename, errorLineNumberOffset = 0, source, entity } = {}) => {
167
- const script = new VMScript(code, filename)
168
-
169
- if (filename != null && source != null) {
170
- sourceFilesInfo.set(filename, { filename, source, entity, errorLineNumberOffset })
171
- }
172
-
173
- // NOTE: if we need to upgrade vm2 we will need to check the source of this function
174
- // in vm2 repo and see if we need to change this,
175
- // we needed to override this method because we want "displayErrors" to be true in order
176
- // to show nice error when the compile of a script fails
177
- script._compile = function (prefix, suffix) {
178
- return new originalVM.Script(prefix + this._compiler(this._prefix + this._code + this._suffix, this.filename) + suffix, {
179
- filename: this.filename,
180
- displayErrors: true,
181
- lineOffset: this.lineOffset,
182
- columnOffset: this.columnOffset
183
- })
184
- }
185
-
186
- try {
187
- const result = await vm.run(script)
188
- return result
189
- } catch (e) {
190
- decorateErrorMessage(e, sourceFilesInfo)
191
-
192
- throw e
193
- }
194
- }
195
- }
196
- }
197
-
198
- function doRequire (moduleName, requirePaths = [], modulesCache) {
199
- const searchedPaths = []
200
-
201
- function safeRequire (require, modulePath) {
202
- // save the current module cache, we will use this to restore the cache to the
203
- // original values after the require finish
204
- const originalModuleCache = Object.assign(Object.create(null), require.cache)
205
-
206
- // clean/empty the current module cache
207
- for (const cacheKey of Object.keys(require.cache)) {
208
- delete require.cache[cacheKey]
209
- }
210
-
211
- // restore any previous cache generated in the sandbox
212
- for (const cacheKey of Object.keys(modulesCache)) {
213
- require.cache[cacheKey] = modulesCache[cacheKey]
214
- }
215
-
216
- try {
217
- const moduleExport = require.main ? require.main.require(modulePath) : require(modulePath)
218
- require.main.children.splice(require.main.children.indexOf(m => m.id === require.resolve(modulePath)), 1)
219
-
220
- // save the current module cache generated after the require into the internal cache,
221
- // and clean the current module cache again
222
- for (const cacheKey of Object.keys(require.cache)) {
223
- modulesCache[cacheKey] = require.cache[cacheKey]
224
- delete require.cache[cacheKey]
225
- }
226
-
227
- // restore the current module cache to the original cache values
228
- for (const [oldCacheKey, value] of Object.entries(originalModuleCache)) {
229
- require.cache[oldCacheKey] = value
230
- }
231
-
232
- return moduleExport
233
- } catch (e) {
234
- // clean the current module cache again
235
- for (const cacheKey of Object.keys(require.cache)) {
236
- delete require.cache[cacheKey]
237
- }
238
-
239
- // restore the current module cache to the original cache values
240
- for (const [oldCacheKey, value] of Object.entries(originalModuleCache)) {
241
- require.cache[oldCacheKey] = value
242
- }
243
-
244
- if (e.code && e.code === 'MODULE_NOT_FOUND') {
245
- if (!searchedPaths.includes(modulePath)) {
246
- searchedPaths.push(modulePath)
247
- }
248
-
249
- return false
250
- } else {
251
- throw new Error(`Unable to require module ${moduleName}. ${e.message}${os.EOL}${e.stack}`)
252
- }
253
- }
254
- }
255
-
256
- let result = safeRequire(require, moduleName)
257
-
258
- if (!result) {
259
- let pathsSearched = 0
260
-
261
- while (!result && pathsSearched < requirePaths.length) {
262
- result = safeRequire(require, path.join(requirePaths[pathsSearched], moduleName))
263
- pathsSearched++
264
- }
265
- }
266
-
267
- if (!result) {
268
- 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}`)
269
- }
270
-
271
- return result
272
- }
273
-
274
- function decorateErrorMessage (e, sourceFilesInfo) {
275
- const filesCount = sourceFilesInfo.size
276
-
277
- if (filesCount > 0) {
278
- const trace = stackTrace.parse(e)
279
- let suffix = ''
280
-
281
- for (let i = 0; i < trace.length; i++) {
282
- const current = trace[i]
283
-
284
- if (
285
- current.getLineNumber() == null &&
286
- current.getColumnNumber() == null
287
- ) {
288
- continue
289
- }
290
-
291
- if (
292
- sourceFilesInfo.has(current.getFileName()) &&
293
- current.getLineNumber() != null
294
- ) {
295
- const { entity: entityAtFile, errorLineNumberOffset: errorLineNumberOffsetForFile } = sourceFilesInfo.get(current.getFileName())
296
- const ln = current.getLineNumber() - errorLineNumberOffsetForFile
297
- if (i === 0) {
298
- if (entityAtFile != null) {
299
- e.entity = {
300
- shortid: entityAtFile.shortid,
301
- name: entityAtFile.name,
302
- content: entityAtFile.content
303
- }
304
-
305
- e.property = 'content'
306
- }
307
-
308
- e.lineNumber = ln < 0 ? null : ln
309
- }
310
- if (ln < 0) {
311
- suffix += `(${current.getFileName()})`
312
- } else {
313
- suffix += `(${current.getFileName()} line ${ln}:${current.getColumnNumber()})`
314
- }
315
- }
316
-
317
- if (
318
- sourceFilesInfo.has(current.getFileName()) &&
319
- current.getLineNumber() != null
320
- ) {
321
- const source = sourceFilesInfo.get(current.getFileName()).source
322
- const codeFrame = codeFrameColumns(source, {
323
- // we don't check if there is column because if it returns empty value then
324
- // the code frame is still generated normally, just without column mark
325
- start: { line: current.getLineNumber(), column: current.getColumnNumber() }
326
- })
327
-
328
- if (codeFrame !== '') {
329
- suffix += `\n\n${codeFrame}\n\n`
330
- }
331
- }
332
- }
333
-
334
- if (suffix !== '') {
335
- e.message = `${e.message}\n\n${suffix}`
336
- }
337
- }
338
-
339
- e.message = `${e.message}`
340
- }
341
-
342
- function getOriginalFromProxy (proxiesInVM, customProxies, value) {
343
- let newValue
344
-
345
- if (customProxies.has(value)) {
346
- newValue = getOriginalFromProxy(proxiesInVM, customProxies, customProxies.get(value))
347
- } else if (proxiesInVM.has(value)) {
348
- newValue = getOriginalFromProxy(proxiesInVM, customProxies, proxiesInVM.get(value))
349
- } else {
350
- newValue = value
351
- }
352
-
353
- return newValue
354
- }
355
-
356
- function copyBasedOnPropertiesConfig (context, propertiesMap) {
357
- const copied = []
358
- const newContext = Object.assign({}, context)
359
-
360
- Object.keys(propertiesMap).sort(sortPropertiesByLevel).forEach((prop) => {
361
- const parts = prop.split('.')
362
- const lastPartsIndex = parts.length - 1
363
-
364
- for (let i = 0; i <= lastPartsIndex; i++) {
365
- let currentContext = newContext
366
- const propName = parts[i]
367
- const parentPath = parts.slice(0, i).join('.')
368
- const fullPropName = parts.slice(0, i + 1).join('.')
369
- let value
370
-
371
- if (copied.indexOf(fullPropName) !== -1) {
372
- continue
373
- }
374
-
375
- if (parentPath !== '') {
376
- currentContext = get(newContext, parentPath)
377
- }
378
-
379
- if (currentContext) {
380
- value = currentContext[propName]
381
-
382
- if (typeof value === 'object') {
383
- if (value === null) {
384
- value = null
385
- } else if (Array.isArray(value)) {
386
- value = Object.assign([], value)
387
- } else {
388
- value = Object.assign({}, value)
389
- }
390
-
391
- currentContext[propName] = value
392
- copied.push(fullPropName)
393
- }
394
- }
395
- }
396
- })
397
-
398
- return newContext
399
- }
400
-
401
- function applyPropertiesConfig (context, config, {
402
- original,
403
- customProxies,
404
- isRoot = true,
405
- isGrouped = true,
406
- onlyReadOnlyTopLevel = false,
407
- parentOpts,
408
- prop
409
- } = {}, readOnlyConfigured = []) {
410
- let isHidden
411
- let isReadOnly
412
- let standalonePropertiesHandled = false
413
- let innerPropertiesHandled = false
414
-
415
- if (isRoot) {
416
- return Object.keys(config).forEach((key) => {
417
- applyPropertiesConfig(context, config[key], {
418
- original,
419
- customProxies,
420
- prop: key,
421
- isRoot: false,
422
- isGrouped: true,
423
- onlyReadOnlyTopLevel,
424
- parentOpts
425
- }, readOnlyConfigured)
426
- })
427
- }
428
-
429
- if (parentOpts && parentOpts.sandboxHidden === true) {
430
- return
431
- }
432
-
433
- if (isGrouped) {
434
- isHidden = config.root ? config.root.sandboxHidden === true : false
435
- isReadOnly = config.root ? config.root.sandboxReadOnly === true : false
436
- } else {
437
- isHidden = config ? config.sandboxHidden === true : false
438
- isReadOnly = config ? config.sandboxReadOnly === true : false
439
- }
440
-
441
- let shouldStoreOriginal = isHidden || isReadOnly
442
-
443
- // prevent storing original value if there is config some child prop
444
- if (
445
- shouldStoreOriginal &&
446
- isGrouped &&
447
- (config.inner != null || config.standalone != null)
448
- ) {
449
- shouldStoreOriginal = false
450
- }
451
-
452
- // saving original value
453
- if (shouldStoreOriginal) {
454
- let exists = true
455
- let newValue
456
-
457
- if (hasOwn(context, prop)) {
458
- const originalPropValue = get(context, prop)
459
-
460
- if (typeof originalPropValue === 'object' && originalPropValue != null) {
461
- if (Array.isArray(originalPropValue)) {
462
- newValue = extend(true, [], originalPropValue)
463
- } else {
464
- newValue = extend(true, {}, originalPropValue)
465
- }
466
- } else {
467
- newValue = originalPropValue
468
- }
469
- } else {
470
- exists = false
471
- }
472
-
473
- original[prop] = {
474
- exists,
475
- value: newValue
476
- }
477
- }
478
-
479
- const processStandAloneProperties = (c) => {
480
- Object.keys(c.standalone).forEach((skey) => {
481
- const sconfig = c.standalone[skey]
482
-
483
- applyPropertiesConfig(context, sconfig, {
484
- original,
485
- customProxies,
486
- prop: skey,
487
- isRoot: false,
488
- isGrouped: false,
489
- onlyReadOnlyTopLevel,
490
- parentOpts: { sandboxHidden: isHidden, sandboxReadOnly: isReadOnly }
491
- }, readOnlyConfigured)
492
- })
493
- }
494
-
495
- const processInnerProperties = (c) => {
496
- Object.keys(c.inner).forEach((ikey) => {
497
- const iconfig = c.inner[ikey]
498
-
499
- applyPropertiesConfig(context, iconfig, {
500
- original,
501
- customProxies,
502
- prop: ikey,
503
- isRoot: false,
504
- isGrouped: true,
505
- parentOpts: { sandboxHidden: isHidden, sandboxReadOnly: isReadOnly }
506
- }, readOnlyConfigured)
507
- })
508
- }
509
-
510
- if (isHidden) {
511
- omitProp(context, prop)
512
- } else if (isReadOnly) {
513
- readOnlyProp(context, prop, readOnlyConfigured, customProxies, {
514
- onlyTopLevel: false,
515
- onBeforeProxy: () => {
516
- if (isGrouped && config.standalone != null) {
517
- processStandAloneProperties(config)
518
- standalonePropertiesHandled = true
519
- }
520
-
521
- if (isGrouped && config.inner != null) {
522
- processInnerProperties(config)
523
- innerPropertiesHandled = true
524
- }
525
- }
526
- })
527
- }
528
-
529
- if (!isGrouped) {
530
- return
531
- }
532
-
533
- // don't process inner config when the value in context is empty
534
- if (get(context, prop) == null) {
535
- return
536
- }
537
-
538
- if (!standalonePropertiesHandled && config.standalone != null) {
539
- processStandAloneProperties(config)
540
- }
541
-
542
- if (!innerPropertiesHandled && config.inner != null) {
543
- processInnerProperties(config)
544
- }
545
- }
546
-
547
- function restoreProperties (context, originalValues, proxiesInVM, customProxies) {
548
- const restored = []
549
- const newContext = Object.assign({}, context)
550
-
551
- Object.keys(originalValues).sort(sortPropertiesByLevel).forEach((prop) => {
552
- const confValue = originalValues[prop]
553
- const parts = prop.split('.')
554
- const lastPartsIndex = parts.length - 1
555
-
556
- for (let i = 0; i <= lastPartsIndex; i++) {
557
- let currentContext = newContext
558
- const propName = parts[i]
559
- const parentPath = parts.slice(0, i).join('.')
560
- const fullPropName = parts.slice(0, i + 1).join('.')
561
- let value
562
-
563
- if (restored.indexOf(fullPropName) !== -1) {
564
- continue
565
- }
566
-
567
- if (parentPath !== '') {
568
- currentContext = get(newContext, parentPath)
569
- }
570
-
571
- if (currentContext) {
572
- value = currentContext[propName]
573
-
574
- // unwrapping proxies
575
- value = getOriginalFromProxy(proxiesInVM, customProxies, value)
576
-
577
- if (typeof value === 'object') {
578
- // we call object assign to be able to get rid of
579
- // previous properties descriptors (hide/readOnly) configured
580
- if (value === null) {
581
- value = null
582
- } else if (Array.isArray(value)) {
583
- value = Object.assign([], value)
584
- } else {
585
- value = Object.assign({}, value)
586
- }
587
-
588
- currentContext[propName] = value
589
- restored.push(fullPropName)
590
- }
591
-
592
- if (i === lastPartsIndex) {
593
- if (confValue.exists) {
594
- currentContext[propName] = confValue.value
595
- } else {
596
- delete currentContext[propName]
597
- }
598
- }
599
- }
600
- }
601
- })
602
-
603
- // unwrapping proxies for top level properties
604
- Object.keys(newContext).forEach((prop) => {
605
- newContext[prop] = getOriginalFromProxy(proxiesInVM, customProxies, newContext[prop])
606
- })
607
-
608
- return newContext
609
- }
610
-
611
- function omitProp (context, prop) {
612
- // if property has value, then set it to undefined first,
613
- // unsetValue expects that property has some non empty value to remove the property
614
- // so we set to "true" to ensure it works for all cases,
615
- // we use unsetValue instead of lodash.omit because
616
- // it supports object paths x.y.z and does not copy the object for each call
617
- if (hasOwn(context, prop)) {
618
- set(context, prop, true)
619
- unsetValue(context, prop)
620
- }
621
- }
622
-
623
- function readOnlyProp (context, prop, configured, customProxies, { onlyTopLevel = false, onBeforeProxy } = {}) {
624
- const parts = prop.split('.')
625
- const lastPartsIndex = parts.length - 1
626
-
627
- const throwError = (fullPropName) => {
628
- throw new Error(`Can't modify read only property "${fullPropName}" inside sandbox`)
629
- }
630
-
631
- for (let i = 0; i <= lastPartsIndex; i++) {
632
- let currentContext = context
633
- const isTopLevelProp = i === 0
634
- const propName = parts[i]
635
- const parentPath = parts.slice(0, i).join('.')
636
- const fullPropName = parts.slice(0, i + 1).join('.')
637
- let value
638
-
639
- if (configured.indexOf(fullPropName) !== -1) {
640
- continue
641
- }
642
-
643
- if (parentPath !== '') {
644
- currentContext = get(context, parentPath)
645
- }
646
-
647
- if (currentContext) {
648
- value = currentContext[propName]
649
-
650
- if (
651
- i === lastPartsIndex &&
652
- typeof value === 'object' &&
653
- value != null
654
- ) {
655
- const valueType = Array.isArray(value) ? 'array' : 'object'
656
- const rawValue = value
657
-
658
- if (onBeforeProxy) {
659
- onBeforeProxy()
660
- }
661
-
662
- value = new Proxy(rawValue, {
663
- set: (target, prop) => {
664
- throw new Error(`Can't add or modify property "${prop}" to read only ${valueType} "${fullPropName}" inside sandbox`)
665
- },
666
- deleteProperty: (target, prop) => {
667
- throw new Error(`Can't delete property "${prop}" in read only ${valueType} "${fullPropName}" inside sandbox`)
668
- }
669
- })
670
-
671
- customProxies.set(value, rawValue)
672
- }
673
-
674
- // only create the getter/setter wrapper if the property is defined,
675
- // this prevents getting errors about proxy traps and descriptors differences
676
- // when calling `JSON.stringify(req.context)` from a script
677
- if (Object.prototype.hasOwnProperty.call(currentContext, propName)) {
678
- if (!configured.includes(fullPropName)) {
679
- configured.push(fullPropName)
680
- }
681
-
682
- Object.defineProperty(currentContext, propName, {
683
- get: () => value,
684
- set: () => { throwError(fullPropName) },
685
- enumerable: true
686
- })
687
- }
688
-
689
- if (isTopLevelProp && onlyTopLevel) {
690
- break
691
- }
692
- }
693
- }
694
- }
695
-
696
- function sortPropertiesByLevel (a, b) {
697
- const parts = a.split('.')
698
- const parts2 = b.split('.')
699
-
700
- return parts.length - parts2.length
701
- }
702
-
703
- function normalizePropertiesConfigInHierarchy (configMap) {
704
- const configMapKeys = Object.keys(configMap)
705
-
706
- const groupedKeys = groupBy(configMapKeys, (key) => {
707
- const parts = key.split('.')
708
-
709
- if (parts.length === 1) {
710
- return ''
711
- }
712
-
713
- return parts.slice(0, -1).join('.')
714
- })
715
-
716
- const hierarchy = []
717
- const hierarchyLevels = {}
718
-
719
- // we sort to ensure that top level properties names are processed first
720
- Object.keys(groupedKeys).sort(sortPropertiesByLevel).forEach((key) => {
721
- if (key === '') {
722
- hierarchy.push('')
723
- return
724
- }
725
-
726
- const parts = key.split('.')
727
- const lastIndexParts = parts.length - 1
728
-
729
- if (parts.length === 1) {
730
- hierarchy.push(parts[0])
731
- hierarchyLevels[key] = {}
732
- return
733
- }
734
-
735
- for (let i = 0; i < parts.length; i++) {
736
- const currentKey = parts.slice(0, i + 1).join('.')
737
- const indexInHierarchy = hierarchy.indexOf(currentKey)
738
- let parentHierarchy = hierarchyLevels
739
-
740
- if (indexInHierarchy === -1 && i === lastIndexParts) {
741
- let parentExistsInTopLevel = false
742
-
743
- for (let j = 0; j < i; j++) {
744
- const segmentedKey = parts.slice(0, j + 1).join('.')
745
-
746
- if (parentExistsInTopLevel !== true) {
747
- parentExistsInTopLevel = hierarchy.indexOf(segmentedKey) !== -1
748
- }
749
-
750
- if (parentHierarchy[segmentedKey] != null) {
751
- parentHierarchy = parentHierarchy[segmentedKey]
752
- }
753
- }
754
-
755
- if (!parentExistsInTopLevel) {
756
- hierarchy.push(key)
757
- }
758
-
759
- parentHierarchy[key] = {}
760
- }
761
- }
762
- })
763
-
764
- const toHierarchyConfigMap = (parentLevels) => {
765
- return (acu, key) => {
766
- if (key === '') {
767
- groupedKeys[key].forEach((g) => {
768
- acu[g] = {}
769
-
770
- if (configMap[g] != null) {
771
- acu[g].root = configMap[g]
772
- }
773
- })
774
-
775
- return acu
776
- }
777
-
778
- const currentLevel = parentLevels[key]
779
-
780
- if (acu[key] == null) {
781
- acu[key] = {}
782
-
783
- if (configMap[key] != null) {
784
- // root is config that was defined in the same property
785
- // that it is grouped
786
- acu[key].root = configMap[key]
787
- }
788
- }
789
-
790
- // standalone are properties that are direct, no groups
791
- acu[key].standalone = groupedKeys[key].reduce((obj, stdProp) => {
792
- // only add the property is not already grouped
793
- if (groupedKeys[stdProp] == null) {
794
- obj[stdProp] = configMap[stdProp]
795
- }
796
-
797
- return obj
798
- }, {})
799
-
800
- if (Object.keys(acu[key].standalone).length === 0) {
801
- delete acu[key].standalone
802
- }
803
-
804
- const levelKeys = Object.keys(currentLevel)
805
-
806
- if (levelKeys.length === 0) {
807
- return acu
808
- }
809
-
810
- // inner are properties which contains other properties, groups
811
- acu[key].inner = levelKeys.reduce(toHierarchyConfigMap(currentLevel), {})
812
-
813
- if (Object.keys(acu[key].inner).length === 0) {
814
- delete acu[key].inner
815
- }
816
-
817
- return acu
818
- }
819
- }
820
-
821
- return hierarchy.reduce(toHierarchyConfigMap(hierarchyLevels), {})
822
- }
1
+ const os = require('os')
2
+ 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')
12
+ const stackTrace = require('stack-trace')
13
+ const { codeFrameColumns } = require('@babel/code-frame')
14
+
15
+ module.exports = (_sandbox, options = {}) => {
16
+ const {
17
+ onLog,
18
+ formatError,
19
+ propertiesConfig = {},
20
+ globalModules = [],
21
+ allowedModules = [],
22
+ requireMap
23
+ } = options
24
+
25
+ const modulesCache = options.modulesCache != null ? options.modulesCache : Object.create(null)
26
+ const _console = {}
27
+
28
+ let requirePaths = options.requirePaths || []
29
+
30
+ requirePaths = requirePaths.filter((p) => p != null).map((p) => {
31
+ if (p.endsWith('/') || p.endsWith('\\')) {
32
+ return p.slice(0, -1)
33
+ }
34
+
35
+ return p
36
+ })
37
+
38
+ // remove duplicates in paths
39
+ requirePaths = requirePaths.filter((v, i) => requirePaths.indexOf(v) === i)
40
+
41
+ function addConsoleMethod (consoleMethod, level) {
42
+ _console[consoleMethod] = function () {
43
+ if (onLog == null) {
44
+ return
45
+ }
46
+
47
+ onLog({
48
+ timestamp: new Date().getTime(),
49
+ level: level,
50
+ message: util.format.apply(util, arguments)
51
+ })
52
+ }
53
+ }
54
+
55
+ addConsoleMethod('log', 'debug')
56
+ addConsoleMethod('warn', 'warn')
57
+ addConsoleMethod('error', 'error')
58
+
59
+ const _require = function (moduleName, { context, allowAllModules = false } = {}) {
60
+ if (requireMap) {
61
+ const mapResult = requireMap(moduleName, { context })
62
+
63
+ if (mapResult != null) {
64
+ return mapResult
65
+ }
66
+ }
67
+
68
+ if (allowAllModules || allowedModules === '*') {
69
+ return doRequire(moduleName, requirePaths, modulesCache)
70
+ }
71
+
72
+ const m = allowedModules.find(mod => (mod.id || mod) === moduleName)
73
+
74
+ if (m) {
75
+ return doRequire(m.path || moduleName, requirePaths, modulesCache)
76
+ }
77
+
78
+ const error = new Error(
79
+ `require of "${moduleName}" module has been blocked.`
80
+ )
81
+
82
+ if (formatError) {
83
+ formatError(error, moduleName)
84
+ }
85
+
86
+ throw error
87
+ }
88
+
89
+ for (const info of globalModules) {
90
+ // it is important to use "doRequire" function here to avoid
91
+ // getting hit by the allowed modules restriction
92
+ _sandbox[info.globalVariableName] = doRequire(info.module, requirePaths, modulesCache)
93
+ }
94
+
95
+ const propsConfig = normalizePropertiesConfigInHierarchy(propertiesConfig)
96
+ const originalValues = {}
97
+ const proxiesInVM = new WeakMap()
98
+ const customProxies = new WeakMap()
99
+
100
+ // we copy the object based on config to avoid sharing same context
101
+ // (with getters/setters) in the rest of request pipeline
102
+ const sandbox = copyBasedOnPropertiesConfig(_sandbox, propertiesConfig)
103
+
104
+ applyPropertiesConfig(sandbox, propsConfig, {
105
+ original: originalValues,
106
+ customProxies
107
+ })
108
+
109
+ Object.assign(sandbox, {
110
+ console: _console,
111
+ require: (m) => _require(m, { context: _sandbox })
112
+ })
113
+
114
+ const vm = new VM()
115
+
116
+ // NOTE: we wrap the Contextify.object, Decontextify.object methods because those are the
117
+ // methods that returns the proxies created by vm2 in the sandbox, we want to have a list of those
118
+ // to later use them
119
+ const wrapAndSaveProxyResult = (originalFn, thisArg) => {
120
+ return (value, ...args) => {
121
+ const result = originalFn.call(thisArg, value, ...args)
122
+
123
+ if (result != null && result.isVMProxy === true) {
124
+ proxiesInVM.set(result, value)
125
+ }
126
+
127
+ return result
128
+ }
129
+ }
130
+
131
+ vm._internal.Contextify.object = wrapAndSaveProxyResult(vm._internal.Contextify.object, vm._internal.Contextify)
132
+ vm._internal.Decontextify.object = wrapAndSaveProxyResult(vm._internal.Decontextify.object, vm._internal.Decontextify)
133
+
134
+ for (const name in sandbox) {
135
+ vm._internal.Contextify.setGlobal(name, sandbox[name])
136
+ }
137
+
138
+ // processing top level props because getter/setter descriptors
139
+ // for top level properties will only work after VM instantiation
140
+ Object.keys(propsConfig).forEach((key) => {
141
+ const currentConfig = propsConfig[key]
142
+
143
+ if (currentConfig.root && currentConfig.root.sandboxReadOnly) {
144
+ readOnlyProp(vm._context, key, [], customProxies, { onlyTopLevel: true })
145
+ }
146
+ })
147
+
148
+ const sourceFilesInfo = new Map()
149
+
150
+ return {
151
+ sandbox: vm._context,
152
+ console: _console,
153
+ contextifyValue: (value) => {
154
+ return vm._internal.Contextify.value(value)
155
+ },
156
+ decontextifyValue: (value) => {
157
+ return vm._internal.Decontextify.value(value)
158
+ },
159
+ restore: () => {
160
+ return restoreProperties(vm._context, originalValues, proxiesInVM, customProxies)
161
+ },
162
+ unproxyValue: (value) => {
163
+ return getOriginalFromProxy(proxiesInVM, customProxies, value)
164
+ },
165
+ safeRequire: (modulePath) => _require(modulePath, { context: _sandbox, allowAllModules: true }),
166
+ run: async (code, { filename, errorLineNumberOffset = 0, source, entity } = {}) => {
167
+ const script = new VMScript(code, filename)
168
+
169
+ if (filename != null && source != null) {
170
+ sourceFilesInfo.set(filename, { filename, source, entity, errorLineNumberOffset })
171
+ }
172
+
173
+ // NOTE: if we need to upgrade vm2 we will need to check the source of this function
174
+ // in vm2 repo and see if we need to change this,
175
+ // we needed to override this method because we want "displayErrors" to be true in order
176
+ // to show nice error when the compile of a script fails
177
+ script._compile = function (prefix, suffix) {
178
+ return new originalVM.Script(prefix + this.getCompiledCode() + suffix, {
179
+ filename: this.filename,
180
+ displayErrors: true,
181
+ lineOffset: this.lineOffset,
182
+ columnOffset: this.columnOffset,
183
+ // THIS FN WAS TAKEN FROM vm2 source, nothing special here
184
+ importModuleDynamically: () => {
185
+ // 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.
186
+ // eslint-disable-next-line no-throw-literal
187
+ throw 'Dynamic imports are not allowed.'
188
+ }
189
+ })
190
+ }
191
+
192
+ try {
193
+ const result = await vm.run(script)
194
+ return result
195
+ } catch (e) {
196
+ decorateErrorMessage(e, sourceFilesInfo)
197
+
198
+ throw e
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ function doRequire (moduleName, requirePaths = [], modulesCache) {
205
+ const searchedPaths = []
206
+
207
+ function safeRequire (require, modulePath) {
208
+ // save the current module cache, we will use this to restore the cache to the
209
+ // original values after the require finish
210
+ const originalModuleCache = Object.assign(Object.create(null), require.cache)
211
+
212
+ // clean/empty the current module cache
213
+ for (const cacheKey of Object.keys(require.cache)) {
214
+ delete require.cache[cacheKey]
215
+ }
216
+
217
+ // restore any previous cache generated in the sandbox
218
+ for (const cacheKey of Object.keys(modulesCache)) {
219
+ require.cache[cacheKey] = modulesCache[cacheKey]
220
+ }
221
+
222
+ try {
223
+ const moduleExport = require.main ? require.main.require(modulePath) : require(modulePath)
224
+ require.main.children.splice(require.main.children.indexOf(m => m.id === require.resolve(modulePath)), 1)
225
+
226
+ // save the current module cache generated after the require into the internal cache,
227
+ // and clean the current module cache again
228
+ for (const cacheKey of Object.keys(require.cache)) {
229
+ modulesCache[cacheKey] = require.cache[cacheKey]
230
+ delete require.cache[cacheKey]
231
+ }
232
+
233
+ // restore the current module cache to the original cache values
234
+ for (const [oldCacheKey, value] of Object.entries(originalModuleCache)) {
235
+ require.cache[oldCacheKey] = value
236
+ }
237
+
238
+ return moduleExport
239
+ } catch (e) {
240
+ // clean the current module cache again
241
+ for (const cacheKey of Object.keys(require.cache)) {
242
+ delete require.cache[cacheKey]
243
+ }
244
+
245
+ // restore the current module cache to the original cache values
246
+ for (const [oldCacheKey, value] of Object.entries(originalModuleCache)) {
247
+ require.cache[oldCacheKey] = value
248
+ }
249
+
250
+ if (e.code && e.code === 'MODULE_NOT_FOUND') {
251
+ if (!searchedPaths.includes(modulePath)) {
252
+ searchedPaths.push(modulePath)
253
+ }
254
+
255
+ return false
256
+ } else {
257
+ throw new Error(`Unable to require module ${moduleName}. ${e.message}${os.EOL}${e.stack}`)
258
+ }
259
+ }
260
+ }
261
+
262
+ let result = safeRequire(require, moduleName)
263
+
264
+ if (!result) {
265
+ let pathsSearched = 0
266
+
267
+ while (!result && pathsSearched < requirePaths.length) {
268
+ result = safeRequire(require, path.join(requirePaths[pathsSearched], moduleName))
269
+ pathsSearched++
270
+ }
271
+ }
272
+
273
+ if (!result) {
274
+ 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}`)
275
+ }
276
+
277
+ return result
278
+ }
279
+
280
+ function decorateErrorMessage (e, sourceFilesInfo) {
281
+ const filesCount = sourceFilesInfo.size
282
+
283
+ if (filesCount > 0) {
284
+ const trace = stackTrace.parse(e)
285
+ let suffix = ''
286
+
287
+ for (let i = 0; i < trace.length; i++) {
288
+ const current = trace[i]
289
+
290
+ if (
291
+ current.getLineNumber() == null &&
292
+ current.getColumnNumber() == null
293
+ ) {
294
+ continue
295
+ }
296
+
297
+ if (
298
+ sourceFilesInfo.has(current.getFileName()) &&
299
+ current.getLineNumber() != null
300
+ ) {
301
+ const { entity: entityAtFile, errorLineNumberOffset: errorLineNumberOffsetForFile } = sourceFilesInfo.get(current.getFileName())
302
+ const ln = current.getLineNumber() - errorLineNumberOffsetForFile
303
+ if (i === 0) {
304
+ if (entityAtFile != null) {
305
+ e.entity = {
306
+ shortid: entityAtFile.shortid,
307
+ name: entityAtFile.name,
308
+ content: entityAtFile.content
309
+ }
310
+
311
+ e.property = 'content'
312
+ }
313
+
314
+ e.lineNumber = ln < 0 ? null : ln
315
+ }
316
+ if (ln < 0) {
317
+ suffix += `(${current.getFileName()})`
318
+ } else {
319
+ suffix += `(${current.getFileName()} line ${ln}:${current.getColumnNumber()})`
320
+ }
321
+ }
322
+
323
+ if (
324
+ sourceFilesInfo.has(current.getFileName()) &&
325
+ current.getLineNumber() != null
326
+ ) {
327
+ const source = sourceFilesInfo.get(current.getFileName()).source
328
+ const codeFrame = codeFrameColumns(source, {
329
+ // we don't check if there is column because if it returns empty value then
330
+ // the code frame is still generated normally, just without column mark
331
+ start: { line: current.getLineNumber(), column: current.getColumnNumber() }
332
+ })
333
+
334
+ if (codeFrame !== '') {
335
+ suffix += `\n\n${codeFrame}\n\n`
336
+ }
337
+ }
338
+ }
339
+
340
+ if (suffix !== '') {
341
+ e.message = `${e.message}\n\n${suffix}`
342
+ }
343
+ }
344
+
345
+ e.message = `${e.message}`
346
+ }
347
+
348
+ function getOriginalFromProxy (proxiesInVM, customProxies, value) {
349
+ let newValue
350
+
351
+ if (customProxies.has(value)) {
352
+ newValue = getOriginalFromProxy(proxiesInVM, customProxies, customProxies.get(value))
353
+ } else if (proxiesInVM.has(value)) {
354
+ newValue = getOriginalFromProxy(proxiesInVM, customProxies, proxiesInVM.get(value))
355
+ } else {
356
+ newValue = value
357
+ }
358
+
359
+ return newValue
360
+ }
361
+
362
+ function copyBasedOnPropertiesConfig (context, propertiesMap) {
363
+ const copied = []
364
+ const newContext = Object.assign({}, context)
365
+
366
+ Object.keys(propertiesMap).sort(sortPropertiesByLevel).forEach((prop) => {
367
+ const parts = prop.split('.')
368
+ const lastPartsIndex = parts.length - 1
369
+
370
+ for (let i = 0; i <= lastPartsIndex; i++) {
371
+ let currentContext = newContext
372
+ const propName = parts[i]
373
+ const parentPath = parts.slice(0, i).join('.')
374
+ const fullPropName = parts.slice(0, i + 1).join('.')
375
+ let value
376
+
377
+ if (copied.indexOf(fullPropName) !== -1) {
378
+ continue
379
+ }
380
+
381
+ if (parentPath !== '') {
382
+ currentContext = get(newContext, parentPath)
383
+ }
384
+
385
+ if (currentContext) {
386
+ value = currentContext[propName]
387
+
388
+ if (typeof value === 'object') {
389
+ if (value === null) {
390
+ value = null
391
+ } else if (Array.isArray(value)) {
392
+ value = Object.assign([], value)
393
+ } else {
394
+ value = Object.assign({}, value)
395
+ }
396
+
397
+ currentContext[propName] = value
398
+ copied.push(fullPropName)
399
+ }
400
+ }
401
+ }
402
+ })
403
+
404
+ return newContext
405
+ }
406
+
407
+ function applyPropertiesConfig (context, config, {
408
+ original,
409
+ customProxies,
410
+ isRoot = true,
411
+ isGrouped = true,
412
+ onlyReadOnlyTopLevel = false,
413
+ parentOpts,
414
+ prop
415
+ } = {}, readOnlyConfigured = []) {
416
+ let isHidden
417
+ let isReadOnly
418
+ let standalonePropertiesHandled = false
419
+ let innerPropertiesHandled = false
420
+
421
+ if (isRoot) {
422
+ return Object.keys(config).forEach((key) => {
423
+ applyPropertiesConfig(context, config[key], {
424
+ original,
425
+ customProxies,
426
+ prop: key,
427
+ isRoot: false,
428
+ isGrouped: true,
429
+ onlyReadOnlyTopLevel,
430
+ parentOpts
431
+ }, readOnlyConfigured)
432
+ })
433
+ }
434
+
435
+ if (parentOpts && parentOpts.sandboxHidden === true) {
436
+ return
437
+ }
438
+
439
+ if (isGrouped) {
440
+ isHidden = config.root ? config.root.sandboxHidden === true : false
441
+ isReadOnly = config.root ? config.root.sandboxReadOnly === true : false
442
+ } else {
443
+ isHidden = config ? config.sandboxHidden === true : false
444
+ isReadOnly = config ? config.sandboxReadOnly === true : false
445
+ }
446
+
447
+ let shouldStoreOriginal = isHidden || isReadOnly
448
+
449
+ // prevent storing original value if there is config some child prop
450
+ if (
451
+ shouldStoreOriginal &&
452
+ isGrouped &&
453
+ (config.inner != null || config.standalone != null)
454
+ ) {
455
+ shouldStoreOriginal = false
456
+ }
457
+
458
+ // saving original value
459
+ if (shouldStoreOriginal) {
460
+ let exists = true
461
+ let newValue
462
+
463
+ if (hasOwn(context, prop)) {
464
+ const originalPropValue = get(context, prop)
465
+
466
+ if (typeof originalPropValue === 'object' && originalPropValue != null) {
467
+ if (Array.isArray(originalPropValue)) {
468
+ newValue = extend(true, [], originalPropValue)
469
+ } else {
470
+ newValue = extend(true, {}, originalPropValue)
471
+ }
472
+ } else {
473
+ newValue = originalPropValue
474
+ }
475
+ } else {
476
+ exists = false
477
+ }
478
+
479
+ original[prop] = {
480
+ exists,
481
+ value: newValue
482
+ }
483
+ }
484
+
485
+ const processStandAloneProperties = (c) => {
486
+ Object.keys(c.standalone).forEach((skey) => {
487
+ const sconfig = c.standalone[skey]
488
+
489
+ applyPropertiesConfig(context, sconfig, {
490
+ original,
491
+ customProxies,
492
+ prop: skey,
493
+ isRoot: false,
494
+ isGrouped: false,
495
+ onlyReadOnlyTopLevel,
496
+ parentOpts: { sandboxHidden: isHidden, sandboxReadOnly: isReadOnly }
497
+ }, readOnlyConfigured)
498
+ })
499
+ }
500
+
501
+ const processInnerProperties = (c) => {
502
+ Object.keys(c.inner).forEach((ikey) => {
503
+ const iconfig = c.inner[ikey]
504
+
505
+ applyPropertiesConfig(context, iconfig, {
506
+ original,
507
+ customProxies,
508
+ prop: ikey,
509
+ isRoot: false,
510
+ isGrouped: true,
511
+ parentOpts: { sandboxHidden: isHidden, sandboxReadOnly: isReadOnly }
512
+ }, readOnlyConfigured)
513
+ })
514
+ }
515
+
516
+ if (isHidden) {
517
+ omitProp(context, prop)
518
+ } else if (isReadOnly) {
519
+ readOnlyProp(context, prop, readOnlyConfigured, customProxies, {
520
+ onlyTopLevel: false,
521
+ onBeforeProxy: () => {
522
+ if (isGrouped && config.standalone != null) {
523
+ processStandAloneProperties(config)
524
+ standalonePropertiesHandled = true
525
+ }
526
+
527
+ if (isGrouped && config.inner != null) {
528
+ processInnerProperties(config)
529
+ innerPropertiesHandled = true
530
+ }
531
+ }
532
+ })
533
+ }
534
+
535
+ if (!isGrouped) {
536
+ return
537
+ }
538
+
539
+ // don't process inner config when the value in context is empty
540
+ if (get(context, prop) == null) {
541
+ return
542
+ }
543
+
544
+ if (!standalonePropertiesHandled && config.standalone != null) {
545
+ processStandAloneProperties(config)
546
+ }
547
+
548
+ if (!innerPropertiesHandled && config.inner != null) {
549
+ processInnerProperties(config)
550
+ }
551
+ }
552
+
553
+ function restoreProperties (context, originalValues, proxiesInVM, customProxies) {
554
+ const restored = []
555
+ const newContext = Object.assign({}, context)
556
+
557
+ Object.keys(originalValues).sort(sortPropertiesByLevel).forEach((prop) => {
558
+ const confValue = originalValues[prop]
559
+ const parts = prop.split('.')
560
+ const lastPartsIndex = parts.length - 1
561
+
562
+ for (let i = 0; i <= lastPartsIndex; i++) {
563
+ let currentContext = newContext
564
+ const propName = parts[i]
565
+ const parentPath = parts.slice(0, i).join('.')
566
+ const fullPropName = parts.slice(0, i + 1).join('.')
567
+ let value
568
+
569
+ if (restored.indexOf(fullPropName) !== -1) {
570
+ continue
571
+ }
572
+
573
+ if (parentPath !== '') {
574
+ currentContext = get(newContext, parentPath)
575
+ }
576
+
577
+ if (currentContext) {
578
+ value = currentContext[propName]
579
+
580
+ // unwrapping proxies
581
+ value = getOriginalFromProxy(proxiesInVM, customProxies, value)
582
+
583
+ if (typeof value === 'object') {
584
+ // we call object assign to be able to get rid of
585
+ // previous properties descriptors (hide/readOnly) configured
586
+ if (value === null) {
587
+ value = null
588
+ } else if (Array.isArray(value)) {
589
+ value = Object.assign([], value)
590
+ } else {
591
+ value = Object.assign({}, value)
592
+ }
593
+
594
+ currentContext[propName] = value
595
+ restored.push(fullPropName)
596
+ }
597
+
598
+ if (i === lastPartsIndex) {
599
+ if (confValue.exists) {
600
+ currentContext[propName] = confValue.value
601
+ } else {
602
+ delete currentContext[propName]
603
+ }
604
+ }
605
+ }
606
+ }
607
+ })
608
+
609
+ // unwrapping proxies for top level properties
610
+ Object.keys(newContext).forEach((prop) => {
611
+ newContext[prop] = getOriginalFromProxy(proxiesInVM, customProxies, newContext[prop])
612
+ })
613
+
614
+ return newContext
615
+ }
616
+
617
+ function omitProp (context, prop) {
618
+ // if property has value, then set it to undefined first,
619
+ // unsetValue expects that property has some non empty value to remove the property
620
+ // so we set to "true" to ensure it works for all cases,
621
+ // we use unsetValue instead of lodash.omit because
622
+ // it supports object paths x.y.z and does not copy the object for each call
623
+ if (hasOwn(context, prop)) {
624
+ set(context, prop, true)
625
+ unsetValue(context, prop)
626
+ }
627
+ }
628
+
629
+ function readOnlyProp (context, prop, configured, customProxies, { onlyTopLevel = false, onBeforeProxy } = {}) {
630
+ const parts = prop.split('.')
631
+ const lastPartsIndex = parts.length - 1
632
+
633
+ const throwError = (fullPropName) => {
634
+ throw new Error(`Can't modify read only property "${fullPropName}" inside sandbox`)
635
+ }
636
+
637
+ for (let i = 0; i <= lastPartsIndex; i++) {
638
+ let currentContext = context
639
+ const isTopLevelProp = i === 0
640
+ const propName = parts[i]
641
+ const parentPath = parts.slice(0, i).join('.')
642
+ const fullPropName = parts.slice(0, i + 1).join('.')
643
+ let value
644
+
645
+ if (configured.indexOf(fullPropName) !== -1) {
646
+ continue
647
+ }
648
+
649
+ if (parentPath !== '') {
650
+ currentContext = get(context, parentPath)
651
+ }
652
+
653
+ if (currentContext) {
654
+ value = currentContext[propName]
655
+
656
+ if (
657
+ i === lastPartsIndex &&
658
+ typeof value === 'object' &&
659
+ value != null
660
+ ) {
661
+ const valueType = Array.isArray(value) ? 'array' : 'object'
662
+ const rawValue = value
663
+
664
+ if (onBeforeProxy) {
665
+ onBeforeProxy()
666
+ }
667
+
668
+ value = new Proxy(rawValue, {
669
+ set: (target, prop) => {
670
+ throw new Error(`Can't add or modify property "${prop}" to read only ${valueType} "${fullPropName}" inside sandbox`)
671
+ },
672
+ deleteProperty: (target, prop) => {
673
+ throw new Error(`Can't delete property "${prop}" in read only ${valueType} "${fullPropName}" inside sandbox`)
674
+ }
675
+ })
676
+
677
+ customProxies.set(value, rawValue)
678
+ }
679
+
680
+ // only create the getter/setter wrapper if the property is defined,
681
+ // this prevents getting errors about proxy traps and descriptors differences
682
+ // when calling `JSON.stringify(req.context)` from a script
683
+ if (Object.prototype.hasOwnProperty.call(currentContext, propName)) {
684
+ if (!configured.includes(fullPropName)) {
685
+ configured.push(fullPropName)
686
+ }
687
+
688
+ Object.defineProperty(currentContext, propName, {
689
+ get: () => value,
690
+ set: () => { throwError(fullPropName) },
691
+ enumerable: true
692
+ })
693
+ }
694
+
695
+ if (isTopLevelProp && onlyTopLevel) {
696
+ break
697
+ }
698
+ }
699
+ }
700
+ }
701
+
702
+ function sortPropertiesByLevel (a, b) {
703
+ const parts = a.split('.')
704
+ const parts2 = b.split('.')
705
+
706
+ return parts.length - parts2.length
707
+ }
708
+
709
+ function normalizePropertiesConfigInHierarchy (configMap) {
710
+ const configMapKeys = Object.keys(configMap)
711
+
712
+ const groupedKeys = groupBy(configMapKeys, (key) => {
713
+ const parts = key.split('.')
714
+
715
+ if (parts.length === 1) {
716
+ return ''
717
+ }
718
+
719
+ return parts.slice(0, -1).join('.')
720
+ })
721
+
722
+ const hierarchy = []
723
+ const hierarchyLevels = {}
724
+
725
+ // we sort to ensure that top level properties names are processed first
726
+ Object.keys(groupedKeys).sort(sortPropertiesByLevel).forEach((key) => {
727
+ if (key === '') {
728
+ hierarchy.push('')
729
+ return
730
+ }
731
+
732
+ const parts = key.split('.')
733
+ const lastIndexParts = parts.length - 1
734
+
735
+ if (parts.length === 1) {
736
+ hierarchy.push(parts[0])
737
+ hierarchyLevels[key] = {}
738
+ return
739
+ }
740
+
741
+ for (let i = 0; i < parts.length; i++) {
742
+ const currentKey = parts.slice(0, i + 1).join('.')
743
+ const indexInHierarchy = hierarchy.indexOf(currentKey)
744
+ let parentHierarchy = hierarchyLevels
745
+
746
+ if (indexInHierarchy === -1 && i === lastIndexParts) {
747
+ let parentExistsInTopLevel = false
748
+
749
+ for (let j = 0; j < i; j++) {
750
+ const segmentedKey = parts.slice(0, j + 1).join('.')
751
+
752
+ if (parentExistsInTopLevel !== true) {
753
+ parentExistsInTopLevel = hierarchy.indexOf(segmentedKey) !== -1
754
+ }
755
+
756
+ if (parentHierarchy[segmentedKey] != null) {
757
+ parentHierarchy = parentHierarchy[segmentedKey]
758
+ }
759
+ }
760
+
761
+ if (!parentExistsInTopLevel) {
762
+ hierarchy.push(key)
763
+ }
764
+
765
+ parentHierarchy[key] = {}
766
+ }
767
+ }
768
+ })
769
+
770
+ const toHierarchyConfigMap = (parentLevels) => {
771
+ return (acu, key) => {
772
+ if (key === '') {
773
+ groupedKeys[key].forEach((g) => {
774
+ acu[g] = {}
775
+
776
+ if (configMap[g] != null) {
777
+ acu[g].root = configMap[g]
778
+ }
779
+ })
780
+
781
+ return acu
782
+ }
783
+
784
+ const currentLevel = parentLevels[key]
785
+
786
+ if (acu[key] == null) {
787
+ acu[key] = {}
788
+
789
+ if (configMap[key] != null) {
790
+ // root is config that was defined in the same property
791
+ // that it is grouped
792
+ acu[key].root = configMap[key]
793
+ }
794
+ }
795
+
796
+ // standalone are properties that are direct, no groups
797
+ acu[key].standalone = groupedKeys[key].reduce((obj, stdProp) => {
798
+ // only add the property is not already grouped
799
+ if (groupedKeys[stdProp] == null) {
800
+ obj[stdProp] = configMap[stdProp]
801
+ }
802
+
803
+ return obj
804
+ }, {})
805
+
806
+ if (Object.keys(acu[key].standalone).length === 0) {
807
+ delete acu[key].standalone
808
+ }
809
+
810
+ const levelKeys = Object.keys(currentLevel)
811
+
812
+ if (levelKeys.length === 0) {
813
+ return acu
814
+ }
815
+
816
+ // inner are properties which contains other properties, groups
817
+ acu[key].inner = levelKeys.reduce(toHierarchyConfigMap(currentLevel), {})
818
+
819
+ if (Object.keys(acu[key].inner).length === 0) {
820
+ delete acu[key].inner
821
+ }
822
+
823
+ return acu
824
+ }
825
+ }
826
+
827
+ return hierarchy.reduce(toHierarchyConfigMap(hierarchyLevels), {})
828
+ }