@jsreport/jsreport-core 3.11.4 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,255 +1,270 @@
1
- const LRU = require('lru-cache')
2
- const stackTrace = require('stack-trace')
3
- const { customAlphabet } = require('nanoid')
4
- const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
5
- const createSandbox = require('./createSandbox')
6
- const normalizeError = require('../../shared/normalizeError')
7
-
8
- module.exports = (reporter) => {
9
- const functionsCache = LRU(reporter.options.sandbox.cache)
10
-
11
- return async ({
12
- manager = {},
13
- context,
14
- userCode,
15
- initFn,
16
- executionFn,
17
- currentPath,
18
- onRequire,
19
- propertiesConfig,
20
- errorLineNumberOffset = 0
21
- }, req) => {
22
- let jsreportProxy = null
23
-
24
- // we use dynamic name because of the potential nested vm2 execution in the jsreportProxy.assets.require
25
- // it may turn out it is a bad approach in assets so we gonna delete it here
26
- const executionFnName = `${nanoid()}_executionFn`
27
-
28
- context[executionFnName] = executionFn
29
- context.__appDirectory = reporter.options.appDirectory
30
- context.__rootDirectory = reporter.options.rootDirectory
31
- context.__parentModuleDirectory = reporter.options.parentModuleDirectory
32
- context.setTimeout = setTimeout
33
- context.__topLevelFunctions = {}
34
- context.__handleError = (err) => handleError(reporter, err)
35
-
36
- const { sourceFilesInfo, run, compileScript, restore, sandbox, sandboxRequire } = createSandbox(context, {
37
- onLog: (log) => {
38
- // we mark any log done in sandbox as userLevel: true, this allows us to detect which logs belongs to user
39
- // and can potentially contain sensitive information
40
-
41
- let consoleType = log.level
42
-
43
- if (consoleType === 'debug') {
44
- consoleType = 'log'
45
- } else if (consoleType === 'warn') {
46
- consoleType = 'warning'
47
- }
48
-
49
- reporter.logger.debug(`(console:${consoleType}) ${log.message}`, { ...req, timestamp: log.timestamp, userLevel: true })
50
- },
51
- formatError: (error, moduleName) => {
52
- error.message += ` To be able to require custom modules you need to add to configuration { "trustUserCode": true } or enable just specific module using { sandbox: { allowedModules": ["${moduleName}"] }`
53
- },
54
- safeExecution: reporter.options.trustUserCode === false,
55
- modulesCache: reporter.requestModulesCache.get(req.context.rootId),
56
- globalModules: reporter.options.sandbox.nativeModules || [],
57
- allowedModules: reporter.options.sandbox.allowedModules,
58
- propertiesConfig,
59
- requirePaths: [
60
- reporter.options.rootDirectory,
61
- reporter.options.appDirectory,
62
- reporter.options.parentModuleDirectory
63
- ],
64
- requireMap: (moduleName) => {
65
- const m = reporter.options.sandbox.modules.find((m) => m.alias === moduleName || m.path === moduleName)
66
-
67
- if (m) {
68
- return require(m.path)
69
- }
70
-
71
- if (moduleName === 'jsreport-proxy') {
72
- return jsreportProxy
73
- }
74
-
75
- if (onRequire) {
76
- return onRequire(moduleName, { context })
77
- }
78
- }
79
- })
80
-
81
- const _getTopLevelFunctions = (code) => {
82
- return getTopLevelFunctions(functionsCache, code)
83
- }
84
-
85
- jsreportProxy = reporter.createProxy({
86
- req,
87
- runInSandbox: run,
88
- context: sandbox,
89
- getTopLevelFunctions: _getTopLevelFunctions,
90
- sandboxRequire
91
- })
92
-
93
- jsreportProxy.currentPath = async () => {
94
- // we get the current path by throwing an error, which give us a stack trace
95
- // which we analyze and see if some source file is associated to an entity
96
- // if it is then we can properly get the path associated to it, if not we
97
- // fallback to the current path passed as options
98
- const filesCount = sourceFilesInfo.size
99
- let resolvedPath = currentPath
100
-
101
- if (filesCount > 0) {
102
- const err = new Error('get me stack trace please')
103
- const trace = stackTrace.parse(err)
104
-
105
- for (let i = 0; i < trace.length; i++) {
106
- const current = trace[i]
107
-
108
- if (sourceFilesInfo.has(current.getFileName())) {
109
- const { entity, entitySet } = sourceFilesInfo.get(current.getFileName())
110
-
111
- if (entity != null && entitySet != null) {
112
- resolvedPath = await reporter.folders.resolveEntityPath(entity, entitySet, req)
113
- break
114
- }
115
- }
116
- }
117
- }
118
-
119
- return resolvedPath
120
- }
121
-
122
- jsreportProxy.currentDirectoryPath = async () => {
123
- const currentPath = await jsreportProxy.currentPath()
124
-
125
- if (currentPath != null) {
126
- const localPath = currentPath.substring(0, currentPath.lastIndexOf('/'))
127
-
128
- if (localPath === '') {
129
- return '/'
130
- }
131
-
132
- return localPath
133
- }
134
-
135
- return currentPath
136
- }
137
-
138
- // NOTE: it is important that cleanup, restore methods are not called from a function attached to the
139
- // sandbox, because the arguments and return value of such function call will be sandboxed again, to solve this
140
- // we don't attach these methods to the sandbox, and instead share them through a "manager" object that should
141
- // be passed in options
142
- manager.restore = restore
143
-
144
- if (typeof initFn === 'function') {
145
- const initScriptInfo = await initFn(_getTopLevelFunctions, compileScript)
146
-
147
- if (initScriptInfo) {
148
- try {
149
- await run(initScriptInfo.script, {
150
- filename: initScriptInfo.filename || 'sandbox-init.js',
151
- source: initScriptInfo.source
152
- })
153
- } catch (e) {
154
- handleError(reporter, e)
155
- }
156
- }
157
- }
158
-
159
- const functionNames = getTopLevelFunctions(functionsCache, userCode)
160
- const functionsCode = `return {${functionNames.map(h => `"${h}": ${h}`).join(',')}}`
161
- const executionCode = `;(async () => { ${userCode} \n\n;${functionsCode} })()
162
- .then((topLevelFunctions) => {
163
- const mergedTopLevelFunctions = { ...topLevelFunctions, ...__topLevelFunctions }
164
-
165
- // expose top level functions to the sandbox context
166
- // so helpers can call other helpers (from shared asset helpers, or .registerHelpers call from proxy)
167
- for (const [topLevelFnName, topLevelFn] of Object.entries(mergedTopLevelFunctions)) {
168
- this[topLevelFnName] = topLevelFn
169
- }
170
-
171
- return ${executionFnName}({
172
- topLevelFunctions: mergedTopLevelFunctions,
173
- require,
174
- console,
175
- context: this
176
- })
177
- }).catch(__handleError);`
178
-
179
- try {
180
- return await run(executionCode, {
181
- filename: 'sandbox.js',
182
- source: userCode,
183
- errorLineNumberOffset
184
- })
185
- } catch (e) {
186
- handleError(reporter, e)
187
- }
188
- }
189
- }
190
-
191
- function handleError (reporter, errValue) {
192
- let newError = normalizeError(errValue)
193
-
194
- if (newError === errValue) {
195
- // here it means the original error was valid in the first place,
196
- // so it was not normalized
197
- newError = new Error(errValue.message)
198
- Object.assign(newError, errValue)
199
-
200
- if (errValue.stack) {
201
- newError.stack = errValue.stack
202
- }
203
- }
204
-
205
- throw reporter.createError(null, {
206
- original: newError,
207
- statusCode: 400
208
- })
209
- }
210
-
211
- function getTopLevelFunctions (cache, code) {
212
- const key = `functions:${code}`
213
-
214
- if (cache.has(key)) {
215
- return cache.get(key)
216
- }
217
-
218
- // lazy load to speed up boot
219
- const parser = require('@babel/parser')
220
- const traverse = require('@babel/traverse').default
221
-
222
- const names = []
223
- try {
224
- const ast = parser.parse(code, {
225
- sourceType: 'script',
226
- allowReturnOutsideFunction: false,
227
- allowAwaitOutsideFunction: true,
228
- plugins: [
229
- 'classProperties',
230
- 'classPrivateProperties',
231
- 'classPrivateMethods',
232
- 'doExpressions',
233
- 'functionBind',
234
- 'throwExpressions',
235
- 'topLevelAwait'
236
- ]
237
- })
238
-
239
- // traverse only function declaration that are defined
240
- // at the top level of program
241
- traverse(ast, {
242
- FunctionDeclaration: (path) => {
243
- if (path.parent.type === 'Program') {
244
- names.push(path.node.id.name)
245
- }
246
- }
247
- })
248
- } catch (e) {
249
- // we let the error handling for later eval
250
- return []
251
- }
252
-
253
- cache.set(key, names)
254
- return names
255
- }
1
+ const LRU = require('lru-cache')
2
+ const stackTrace = require('stack-trace')
3
+ const { customAlphabet } = require('nanoid')
4
+ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
5
+ const createSandbox = require('./createSandbox')
6
+ const normalizeError = require('../../shared/normalizeError')
7
+
8
+ module.exports = function createRunInSandbox (reporter) {
9
+ const functionsCache = LRU(reporter.options.sandbox.cache)
10
+
11
+ return async function runInSandbox ({
12
+ manager = {},
13
+ context,
14
+ userCode,
15
+ initFn,
16
+ executionFn,
17
+ currentPath,
18
+ onRequire,
19
+ propertiesConfig,
20
+ errorLineNumberOffset = 0
21
+ }, req) {
22
+ let jsreportProxy = null
23
+
24
+ // we use dynamic name because of the potential nested vm execution in the jsreportProxy.assets.require
25
+ // it may turn out it is a bad approach in assets so we gonna delete it here
26
+ const executionFnName = `${nanoid()}_executionFn`
27
+
28
+ context[executionFnName] = executionFn
29
+ context.__appDirectory = reporter.options.appDirectory
30
+ context.__rootDirectory = reporter.options.rootDirectory
31
+ context.__parentModuleDirectory = reporter.options.parentModuleDirectory
32
+ context.__topLevelFunctions = {}
33
+ context.__handleError = (err) => handleError(reporter, err)
34
+
35
+ const { sourceFilesInfo, run, compileScript, restore, sandbox, sandboxRequire } = await createSandbox(context, {
36
+ rootDirectory: reporter.options.rootDirectory,
37
+ onLog: (log) => {
38
+ // we mark any log done in sandbox as userLevel: true, this allows us to detect which logs belongs to user
39
+ // and can potentially contain sensitive information
40
+
41
+ let consoleType = log.level
42
+
43
+ if (consoleType === 'debug') {
44
+ consoleType = 'log'
45
+ } else if (consoleType === 'warn') {
46
+ consoleType = 'warning'
47
+ }
48
+
49
+ reporter.logger.debug(`(console:${consoleType}) ${log.message}`, { ...req, timestamp: log.timestamp, userLevel: true })
50
+ },
51
+ formatError: (error, moduleName) => {
52
+ error.message += ` To be able to require custom modules you need to add to configuration { "trustUserCode": true } or enable just specific module using { sandbox: { allowedModules": ["${moduleName}"] }`
53
+ },
54
+ safeExecution: reporter.options.trustUserCode === false,
55
+ isolateModules: reporter.options.sandbox.isolateModules !== false,
56
+ modulesCache: reporter.requestModulesCache.get(req.context.rootId),
57
+ globalModules: reporter.options.sandbox.nativeModules || [],
58
+ allowedModules: reporter.options.sandbox.allowedModules,
59
+ propertiesConfig,
60
+ requirePaths: [
61
+ reporter.options.rootDirectory,
62
+ reporter.options.appDirectory,
63
+ reporter.options.parentModuleDirectory
64
+ ],
65
+ requireMap: (moduleName) => {
66
+ const m = reporter.options.sandbox.modules.find((m) => m.alias === moduleName || m.path === moduleName)
67
+
68
+ if (m) {
69
+ return require(m.path)
70
+ }
71
+
72
+ if (moduleName === 'jsreport-proxy') {
73
+ return jsreportProxy
74
+ }
75
+
76
+ if (onRequire) {
77
+ return onRequire(moduleName, { context })
78
+ }
79
+ }
80
+ })
81
+
82
+ const _getTopLevelFunctions = function _getTopLevelFunctions (code) {
83
+ return getTopLevelFunctions(functionsCache, code)
84
+ }
85
+
86
+ jsreportProxy = reporter.createProxy({
87
+ req,
88
+ runInSandbox: run,
89
+ context: sandbox,
90
+ getTopLevelFunctions: _getTopLevelFunctions,
91
+ sandboxRequire
92
+ })
93
+
94
+ jsreportProxy.currentPath = async function getCurrentPath () {
95
+ // we get the current path by throwing an error, which give us a stack trace
96
+ // which we analyze and see if some source file is associated to an entity
97
+ // if it is then we can properly get the path associated to it, if not we
98
+ // fallback to the current path passed as options
99
+ const filesCount = sourceFilesInfo.size
100
+ let resolvedPath = currentPath
101
+
102
+ if (filesCount > 0) {
103
+ const err = new Error('get me stack trace please')
104
+ const trace = stackTrace.parse(err)
105
+
106
+ for (let i = 0; i < trace.length; i++) {
107
+ const current = trace[i]
108
+
109
+ if (sourceFilesInfo.has(current.getFileName())) {
110
+ const { entity, entitySet } = sourceFilesInfo.get(current.getFileName())
111
+
112
+ if (entity != null && entitySet != null) {
113
+ resolvedPath = await reporter.folders.resolveEntityPath(entity, entitySet, req)
114
+ break
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ return resolvedPath
121
+ }
122
+
123
+ jsreportProxy.currentDirectoryPath = async function getCurrentDirectoryPath () {
124
+ const currentPath = await jsreportProxy.currentPath()
125
+
126
+ if (currentPath != null) {
127
+ const localPath = currentPath.substring(0, currentPath.lastIndexOf('/'))
128
+
129
+ if (localPath === '') {
130
+ return '/'
131
+ }
132
+
133
+ return localPath
134
+ }
135
+
136
+ return currentPath
137
+ }
138
+
139
+ // NOTE: it is important that cleanup, restore methods are not called from a function attached to the
140
+ // sandbox, because the arguments and return value of such function call will be sandboxed again, to solve this
141
+ // we don't attach these methods to the sandbox, and instead share them through a "manager" object that should
142
+ // be passed in options
143
+ manager.restore = restore
144
+
145
+ if (typeof initFn === 'function') {
146
+ const initScriptInfo = await initFn(_getTopLevelFunctions, compileScript)
147
+
148
+ if (initScriptInfo) {
149
+ try {
150
+ await run(initScriptInfo.script, {
151
+ filename: initScriptInfo.filename || 'sandbox-init.js',
152
+ source: initScriptInfo.source
153
+ })
154
+ } catch (e) {
155
+ handleError(reporter, e)
156
+ }
157
+ }
158
+ }
159
+
160
+ const functionNames = getTopLevelFunctions(functionsCache, userCode)
161
+
162
+ // it is better we remove our internal functions so we avoid user having the chance
163
+ // to call them, as long as we force the execution to be truly async (with the await 1)
164
+ // then it is safe to delete __handleError from context, when the execution is truly
165
+ // async then it means the __handleError was already passed to catch handler,
166
+ // therefore safe to delete
167
+ const contextNormalizeCode = [
168
+ 'await 1;',
169
+ `const ${executionFnName}_expose = ${executionFnName};`,
170
+ 'delete this.__handleError;',
171
+ `delete this['${executionFnName}'];`
172
+ ].join('')
173
+
174
+ const functionsCode = `return {topLevelFunctions: {${functionNames.map(h => `"${h}": ${h}`).join(',')}}, fnToExecute: ${executionFnName}_expose}`
175
+
176
+ const executionCode = `;(async () => { ${contextNormalizeCode}${userCode} \n\n;${functionsCode} })()
177
+ .then(({ topLevelFunctions, fnToExecute }) => {
178
+ const mergedTopLevelFunctions = { ...topLevelFunctions, ...__topLevelFunctions }
179
+
180
+ // expose top level functions to the sandbox context
181
+ // so helpers can call other helpers (from shared asset helpers, or .registerHelpers call from proxy)
182
+ for (const [topLevelFnName, topLevelFn] of Object.entries(mergedTopLevelFunctions)) {
183
+ this[topLevelFnName] = topLevelFn
184
+ }
185
+
186
+ return fnToExecute({
187
+ topLevelFunctions: mergedTopLevelFunctions,
188
+ require,
189
+ console,
190
+ context: this
191
+ })
192
+ }).catch(__handleError);`
193
+
194
+ try {
195
+ return await run(executionCode, {
196
+ filename: 'sandbox.js',
197
+ source: userCode,
198
+ errorLineNumberOffset
199
+ })
200
+ } catch (e) {
201
+ handleError(reporter, e)
202
+ }
203
+ }
204
+ }
205
+
206
+ function handleError (reporter, errValue) {
207
+ let newError = normalizeError(errValue)
208
+
209
+ if (newError === errValue) {
210
+ // here it means the original error was valid in the first place,
211
+ // so it was not normalized
212
+ newError = new Error(errValue.message)
213
+ Object.assign(newError, errValue)
214
+
215
+ if (errValue.stack) {
216
+ newError.stack = errValue.stack
217
+ }
218
+ }
219
+
220
+ throw reporter.createError(null, {
221
+ original: newError,
222
+ statusCode: 400
223
+ })
224
+ }
225
+
226
+ function getTopLevelFunctions (cache, code) {
227
+ const key = `functions:${code}`
228
+
229
+ if (cache.has(key)) {
230
+ return cache.get(key)
231
+ }
232
+
233
+ // lazy load to speed up boot
234
+ const parser = require('@babel/parser')
235
+ const traverse = require('@babel/traverse').default
236
+
237
+ const names = []
238
+ try {
239
+ const ast = parser.parse(code, {
240
+ sourceType: 'script',
241
+ allowReturnOutsideFunction: false,
242
+ allowAwaitOutsideFunction: true,
243
+ plugins: [
244
+ 'classProperties',
245
+ 'classPrivateProperties',
246
+ 'classPrivateMethods',
247
+ 'doExpressions',
248
+ 'functionBind',
249
+ 'throwExpressions',
250
+ 'topLevelAwait'
251
+ ]
252
+ })
253
+
254
+ // traverse only function declaration that are defined
255
+ // at the top level of program
256
+ traverse(ast, {
257
+ FunctionDeclaration: (path) => {
258
+ if (path.parent.type === 'Program') {
259
+ names.push(path.node.id.name)
260
+ }
261
+ }
262
+ })
263
+ } catch (e) {
264
+ // we let the error handling for later eval
265
+ return []
266
+ }
267
+
268
+ cache.set(key, names)
269
+ return names
270
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "3.11.4",
3
+ "version": "4.0.0",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -40,9 +40,11 @@
40
40
  "@babel/parser": "7.14.4",
41
41
  "@babel/traverse": "7.12.9",
42
42
  "@colors/colors": "1.5.0",
43
- "@jsreport/advanced-workers": "1.2.4",
43
+ "@jsreport/advanced-workers": "2.0.0",
44
44
  "@jsreport/mingo": "2.4.1",
45
45
  "@jsreport/reap": "0.1.0",
46
+ "@jsreport/serializator": "1.0.0",
47
+ "@jsreport/ses": "1.0.0",
46
48
  "ajv": "6.12.6",
47
49
  "app-root-path": "3.0.0",
48
50
  "bytes": "3.1.2",
@@ -65,18 +67,17 @@
65
67
  "nanoid": "3.2.0",
66
68
  "nconf": "0.12.0",
67
69
  "node.extend.without.arrays": "1.1.6",
68
- "semver": "7.3.5",
69
- "serializator": "1.0.2",
70
+ "semver": "7.5.4",
70
71
  "stack-trace": "0.0.10",
71
72
  "triple-beam": "1.3.0",
72
73
  "unset-value": "2.0.1",
73
74
  "uuid": "8.3.2",
74
- "vm2": "3.9.17",
75
75
  "winston": "3.8.1",
76
76
  "winston-transport": "4.5.0",
77
77
  "yieldable-json": "2.0.1"
78
78
  },
79
79
  "devDependencies": {
80
+ "@node-rs/jsonwebtoken": "0.2.0",
80
81
  "mocha": "9.2.2",
81
82
  "should": "13.2.3",
82
83
  "standard": "16.0.4",
@@ -84,7 +85,7 @@
84
85
  "winston-loggly-bulk": "3.2.1"
85
86
  },
86
87
  "engines": {
87
- "node": ">=16.11"
88
+ "node": ">=18.15"
88
89
  },
89
90
  "standard": {
90
91
  "env": {