@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.
- package/LICENSE +166 -166
- package/README.md +298 -298
- package/index.js +29 -29
- package/lib/main/blobStorage/blobStorage.js +52 -52
- package/lib/main/blobStorage/inMemoryProvider.js +27 -27
- package/lib/main/blobStorage/mainActions.js +24 -24
- package/lib/main/createDefaultLoggerFormat.js +17 -17
- package/lib/main/defaults.js +14 -14
- package/lib/main/extensions/discover.js +20 -20
- package/lib/main/extensions/extensionsManager.js +264 -264
- package/lib/main/extensions/fileUtils.js +56 -56
- package/lib/main/extensions/findVersion.js +49 -49
- package/lib/main/extensions/locationCache.js +103 -103
- package/lib/main/extensions/sorter.js +10 -10
- package/lib/main/extensions/validateMinimalVersion.js +50 -50
- package/lib/main/folders/cascadeFolderRemove.js +25 -25
- package/lib/main/folders/getEntitiesInFolder.js +53 -53
- package/lib/main/folders/index.js +42 -42
- package/lib/main/folders/moveBetweenFolders.js +354 -354
- package/lib/main/folders/validateDuplicatedName.js +107 -107
- package/lib/main/folders/validateReservedName.js +53 -53
- package/lib/main/logger.js +244 -244
- package/lib/main/migration/resourcesToAssets.js +230 -230
- package/lib/main/migration/xlsxTemplatesToAssets.js +128 -128
- package/lib/main/monitoring.js +91 -91
- package/lib/main/optionsLoad.js +237 -237
- package/lib/main/optionsSchema.js +237 -237
- package/lib/main/reporter.js +574 -579
- package/lib/main/schemaValidator.js +252 -252
- package/lib/main/settings.js +154 -154
- package/lib/main/store/checkDuplicatedId.js +27 -27
- package/lib/main/store/collection.js +329 -329
- package/lib/main/store/documentStore.js +469 -469
- package/lib/main/store/mainActions.js +28 -28
- package/lib/main/store/memoryStoreProvider.js +99 -99
- package/lib/main/store/queue.js +48 -48
- package/lib/main/store/referenceUtils.js +251 -251
- package/lib/main/store/setupValidateId.js +43 -43
- package/lib/main/store/setupValidateShortid.js +71 -71
- package/lib/main/store/transaction.js +69 -69
- package/lib/main/store/typeUtils.js +180 -180
- package/lib/main/templates.js +34 -34
- package/lib/main/validateEntityName.js +62 -62
- package/lib/shared/createError.js +36 -36
- package/lib/shared/encryption.js +114 -114
- package/lib/shared/folders/index.js +11 -11
- package/lib/shared/folders/normalizeEntityPath.js +15 -15
- package/lib/shared/folders/resolveEntityFromPath.js +88 -88
- package/lib/shared/folders/resolveEntityPath.js +46 -46
- package/lib/shared/folders/resolveFolderFromPath.js +38 -38
- package/lib/shared/generateRequestId.js +4 -4
- package/lib/shared/listenerCollection.js +169 -169
- package/lib/shared/normalizeMetaFromLogs.js +30 -30
- package/lib/shared/reporter.js +128 -123
- package/lib/shared/request.js +64 -64
- package/lib/shared/tempFilesHandler.js +81 -81
- package/lib/shared/templates.js +82 -82
- package/lib/static/helpers.js +33 -33
- package/lib/worker/blobStorage.js +34 -34
- package/lib/worker/defaultProxyExtend.js +46 -46
- package/lib/worker/documentStore.js +49 -49
- package/lib/worker/extensionsManager.js +17 -17
- package/lib/worker/logger.js +48 -48
- package/lib/worker/render/diff.js +138 -138
- package/lib/worker/render/executeEngine.js +239 -207
- package/lib/worker/render/htmlRecipe.js +10 -10
- package/lib/worker/render/moduleHelper.js +45 -43
- package/lib/worker/render/noneEngine.js +12 -12
- package/lib/worker/render/profiler.js +158 -158
- package/lib/worker/render/render.js +202 -205
- package/lib/worker/render/resolveReferences.js +60 -60
- package/lib/worker/reporter.js +192 -191
- package/lib/worker/sandbox/runInSandbox.js +16 -9
- package/lib/worker/sandbox/safeSandbox.js +828 -828
- package/lib/worker/templates.js +80 -78
- package/lib/worker/workerHandler.js +54 -54
- package/package.json +92 -92
- package/test/blobStorage/common.js +21 -21
- 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
|
+
}
|