@jsreport/jsreport-core 3.1.2-test.2 → 3.2.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.
Files changed (79) hide show
  1. package/LICENSE +166 -166
  2. package/README.md +298 -298
  3. package/index.js +29 -29
  4. package/lib/main/blobStorage/blobStorage.js +52 -52
  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 -264
  11. package/lib/main/extensions/fileUtils.js +56 -56
  12. package/lib/main/extensions/findVersion.js +49 -49
  13. package/lib/main/extensions/locationCache.js +103 -103
  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 -230
  24. package/lib/main/migration/xlsxTemplatesToAssets.js +128 -128
  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 +574 -579
  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 -169
  53. package/lib/shared/normalizeMetaFromLogs.js +30 -30
  54. package/lib/shared/reporter.js +128 -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 +239 -207
  66. package/lib/worker/render/htmlRecipe.js +10 -10
  67. package/lib/worker/render/moduleHelper.js +45 -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 +202 -205
  71. package/lib/worker/render/resolveReferences.js +60 -60
  72. package/lib/worker/reporter.js +192 -191
  73. package/lib/worker/sandbox/runInSandbox.js +16 -9
  74. package/lib/worker/sandbox/safeSandbox.js +828 -828
  75. package/lib/worker/templates.js +80 -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,828 +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.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
- }
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
+ }