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