@jsreport/jsreport-core 4.2.2 → 4.3.1
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/README.md
CHANGED
|
@@ -256,7 +256,7 @@ jsreport currently support these main listeners
|
|
|
256
256
|
- `closeListeners()` called when jsreport is about to be closed, you will usually put here some code that clean up some resource
|
|
257
257
|
|
|
258
258
|
## Studio
|
|
259
|
-
jsreport includes also visual html studio and rest API. This is provided through two extensions, [@jsreport/jsreport-express](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-express) extension to have a web server available and [@jsreport/jsreport-studio](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-studio) for the web UI, both extensions should be installed in order to have the studio ready. See the documentation of each extension for the details
|
|
259
|
+
jsreport includes also visual html studio and rest API. This is provided through two extensions, [@jsreport/jsreport-express](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-express) extension to have a web server available and [@jsreport/jsreport-studio](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-studio) for the web UI, both extensions should be installed in order to have the studio ready. See the documentation of each extension for the details
|
|
260
260
|
|
|
261
261
|
## Template store
|
|
262
262
|
`jsreport-core` includes API for persisting and accessing report templates. This API is then used by extensions mainly in combination with jsreport [studio](#studio). `jsreport-core` implements just in-memory persistence, but you can add other persistence methods through extensions, see the [template stores](https://jsreport.net/learn/template-stores) docummentation
|
|
@@ -282,11 +282,21 @@ jsreport.documentStore.collection('templates')
|
|
|
282
282
|
|
|
283
283
|
## Changelog
|
|
284
284
|
|
|
285
|
+
### 4.3.1
|
|
286
|
+
|
|
287
|
+
- fix `waitForAsyncHelper`, `waitForAsyncHelpers` not working with trustUserCode: true
|
|
288
|
+
|
|
289
|
+
### 4.3.0
|
|
290
|
+
|
|
291
|
+
- expose safe properties of `req.context.user` in sandbox
|
|
292
|
+
- fix component execution when wrapped with async helper
|
|
293
|
+
- fix jsdom require in sandbox
|
|
294
|
+
|
|
285
295
|
### 4.2.2
|
|
286
296
|
|
|
287
297
|
- fix recursive component rendering
|
|
288
298
|
|
|
289
|
-
###
|
|
299
|
+
### 4.2.1
|
|
290
300
|
|
|
291
301
|
- response.output.update now accepts Web ReadableStream (which is what new version of puppeteer returns)
|
|
292
302
|
|
|
@@ -14,7 +14,7 @@ module.exports = (reporter) => {
|
|
|
14
14
|
|
|
15
15
|
reporter.templatingEngines = { cache: templatesCache }
|
|
16
16
|
|
|
17
|
-
const contextExecutionChainMap = new
|
|
17
|
+
const contextExecutionChainMap = new Map()
|
|
18
18
|
const executionFnParsedParamsMap = new Map()
|
|
19
19
|
const executionAsyncResultsMap = new Map()
|
|
20
20
|
const executionAsyncCallChainMap = new Map()
|
|
@@ -69,7 +69,7 @@ module.exports = (reporter) => {
|
|
|
69
69
|
return templatingEnginesEvaluate(false, executionInfo, entityInfo, req)
|
|
70
70
|
},
|
|
71
71
|
waitForAsyncHelper: async (maybeAsyncContent) => {
|
|
72
|
-
const executionChain = contextExecutionChainMap.get(context) || []
|
|
72
|
+
const executionChain = contextExecutionChainMap.get(context.__sandboxId) || []
|
|
73
73
|
const executionId = executionChain[executionChain.length - 1]
|
|
74
74
|
|
|
75
75
|
if (
|
|
@@ -99,7 +99,7 @@ module.exports = (reporter) => {
|
|
|
99
99
|
return content
|
|
100
100
|
},
|
|
101
101
|
waitForAsyncHelpers: async () => {
|
|
102
|
-
const executionChain = contextExecutionChainMap.get(context) || []
|
|
102
|
+
const executionChain = contextExecutionChainMap.get(context.__sandboxId) || []
|
|
103
103
|
const executionId = executionChain[executionChain.length - 1]
|
|
104
104
|
|
|
105
105
|
if (executionId != null && executionAsyncResultsMap.has(executionId)) {
|
|
@@ -115,7 +115,7 @@ module.exports = (reporter) => {
|
|
|
115
115
|
}
|
|
116
116
|
},
|
|
117
117
|
addFinishListener: (fn) => {
|
|
118
|
-
const executionChain = contextExecutionChainMap.get(context) || []
|
|
118
|
+
const executionChain = contextExecutionChainMap.get(context.__sandboxId) || []
|
|
119
119
|
const executionId = executionChain[executionChain.length - 1]
|
|
120
120
|
|
|
121
121
|
if (executionId && executionFinishListenersMap.has(executionId)) {
|
|
@@ -123,7 +123,7 @@ module.exports = (reporter) => {
|
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
createAsyncHelperResult: (v) => {
|
|
126
|
-
const executionChain = contextExecutionChainMap.get(context) || []
|
|
126
|
+
const executionChain = contextExecutionChainMap.get(context.__sandboxId) || []
|
|
127
127
|
const executionId = executionChain[executionChain.length - 1]
|
|
128
128
|
|
|
129
129
|
const asyncResultMap = executionAsyncResultsMap.get(executionId)
|
|
@@ -170,6 +170,7 @@ module.exports = (reporter) => {
|
|
|
170
170
|
|
|
171
171
|
const normalizedHelpers = `${helpers || ''}`
|
|
172
172
|
const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${normalizedHelpers}`
|
|
173
|
+
let sandboxId
|
|
173
174
|
|
|
174
175
|
const initFn = async (getTopLevelFunctions, compileScript) => {
|
|
175
176
|
if (systemHelpersCache != null) {
|
|
@@ -213,14 +214,15 @@ module.exports = (reporter) => {
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
const executionFn = async ({ require, console, topLevelFunctions, context }) => {
|
|
217
|
+
sandboxId = context.__sandboxId
|
|
216
218
|
const asyncResultMap = new Map()
|
|
217
219
|
const asyncCallChainSet = new Set()
|
|
218
220
|
|
|
219
|
-
if (!contextExecutionChainMap.has(
|
|
220
|
-
contextExecutionChainMap.set(
|
|
221
|
+
if (!contextExecutionChainMap.has(sandboxId)) {
|
|
222
|
+
contextExecutionChainMap.set(sandboxId, [])
|
|
221
223
|
}
|
|
222
224
|
|
|
223
|
-
contextExecutionChainMap.get(
|
|
225
|
+
contextExecutionChainMap.get(sandboxId).push(executionId)
|
|
224
226
|
|
|
225
227
|
executionAsyncResultsMap.set(executionId, asyncResultMap)
|
|
226
228
|
executionAsyncCallChainMap.set(executionId, asyncCallChainSet)
|
|
@@ -259,16 +261,29 @@ module.exports = (reporter) => {
|
|
|
259
261
|
const resolvedResultsMap = new Map()
|
|
260
262
|
|
|
261
263
|
// we need to use the cloned map, because there can be a waitForAsyncHelper pending that needs the asyncResultMap values
|
|
262
|
-
|
|
264
|
+
let clonedMap = new Map(asyncResultMap)
|
|
265
|
+
|
|
263
266
|
while (clonedMap.size > 0) {
|
|
264
|
-
|
|
267
|
+
const keysEvaluated = [...clonedMap.keys()]
|
|
268
|
+
|
|
269
|
+
await Promise.all(keysEvaluated.map(async (k) => {
|
|
265
270
|
const result = await clonedMap.get(k)
|
|
266
271
|
asyncCallChainSet.delete(k)
|
|
267
272
|
resolvedResultsMap.set(k, `${result}`)
|
|
268
273
|
clonedMap.delete(k)
|
|
269
274
|
}))
|
|
275
|
+
|
|
276
|
+
// we need to remove the keys processed from the original map at this point
|
|
277
|
+
// (after the await) because during the async work the asyncResultMap will be read
|
|
278
|
+
for (const k of keysEvaluated) {
|
|
279
|
+
asyncResultMap.delete(k)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// we want to process the new generated pending async results
|
|
283
|
+
if (asyncResultMap.size > 0) {
|
|
284
|
+
clonedMap = new Map(asyncResultMap)
|
|
285
|
+
}
|
|
270
286
|
}
|
|
271
|
-
asyncResultMap.clear()
|
|
272
287
|
|
|
273
288
|
while (contentResult.includes('{#asyncHelperResult')) {
|
|
274
289
|
contentResult = contentResult.replace(/{#asyncHelperResult ([^{}]+)}/g, (str, p1) => {
|
|
@@ -284,11 +299,12 @@ module.exports = (reporter) => {
|
|
|
284
299
|
return `${resolvedResultsMap.get(asyncResultId)}`
|
|
285
300
|
})
|
|
286
301
|
}
|
|
302
|
+
|
|
287
303
|
contentResult = contentResult.replace(/asyncUnresolvedHelperResult/g, 'asyncHelperResult')
|
|
288
304
|
|
|
289
305
|
await executionFinishListenersMap.get(executionId).fire()
|
|
290
306
|
|
|
291
|
-
contextExecutionChainMap.set(
|
|
307
|
+
contextExecutionChainMap.set(sandboxId, contextExecutionChainMap.get(sandboxId).filter((id) => id !== executionId))
|
|
292
308
|
|
|
293
309
|
return {
|
|
294
310
|
// handlebars escapes single brackets before execution to prevent errors on {#asset}
|
|
@@ -373,6 +389,10 @@ module.exports = (reporter) => {
|
|
|
373
389
|
}
|
|
374
390
|
|
|
375
391
|
throw newError
|
|
392
|
+
} finally {
|
|
393
|
+
if (sandboxId != null) {
|
|
394
|
+
contextExecutionChainMap.delete(sandboxId)
|
|
395
|
+
}
|
|
376
396
|
}
|
|
377
397
|
}
|
|
378
398
|
|
|
@@ -253,6 +253,23 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
|
|
|
253
253
|
shouldStoreOriginal = false
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
// allow configuring parent as hidden, but child props as readonly so we expose
|
|
257
|
+
// just part of the parent
|
|
258
|
+
// const ignoreParentHidden = false
|
|
259
|
+
const ignoreParentHidden = (
|
|
260
|
+
isHidden &&
|
|
261
|
+
isGrouped &&
|
|
262
|
+
(hierarchyPropertiesConfig.inner != null || hierarchyPropertiesConfig.standalone != null)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if (parentOpts?.originalSandboxHidden !== true && ignoreParentHidden) {
|
|
266
|
+
// we store the original value for the top parent with hidden value
|
|
267
|
+
shouldStoreOriginal = true
|
|
268
|
+
} else if (parentOpts && parentOpts.originalSandboxHidden === true) {
|
|
269
|
+
// we don't store original value when parent was configured as hidden
|
|
270
|
+
shouldStoreOriginal = false
|
|
271
|
+
}
|
|
272
|
+
|
|
256
273
|
// saving original value
|
|
257
274
|
if (shouldStoreOriginal) {
|
|
258
275
|
let exists = true
|
|
@@ -284,6 +301,19 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
|
|
|
284
301
|
Object.keys(c.standalone).forEach((standaloneKey) => {
|
|
285
302
|
const standaloneConfig = c.standalone[standaloneKey]
|
|
286
303
|
|
|
304
|
+
const newParentOpts = {
|
|
305
|
+
sandboxHidden: ignoreParentHidden ? false : isHidden,
|
|
306
|
+
sandboxReadOnly: isReadOnly
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// set and inherit originalSandboxHidden if needed
|
|
310
|
+
if (
|
|
311
|
+
parentOpts?.originalSandboxHidden === true ||
|
|
312
|
+
(ignoreParentHidden && isHidden)
|
|
313
|
+
) {
|
|
314
|
+
newParentOpts.originalSandboxHidden = true
|
|
315
|
+
}
|
|
316
|
+
|
|
287
317
|
applyPropertiesConfig(context, standaloneConfig, {
|
|
288
318
|
original,
|
|
289
319
|
customProxies,
|
|
@@ -291,7 +321,7 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
|
|
|
291
321
|
isRoot: false,
|
|
292
322
|
isGrouped: false,
|
|
293
323
|
onlyReadOnlyTopLevel,
|
|
294
|
-
parentOpts:
|
|
324
|
+
parentOpts: newParentOpts
|
|
295
325
|
}, readOnlyConfigured)
|
|
296
326
|
})
|
|
297
327
|
}
|
|
@@ -300,19 +330,65 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
|
|
|
300
330
|
Object.keys(c.inner).forEach((innerKey) => {
|
|
301
331
|
const innerConfig = c.inner[innerKey]
|
|
302
332
|
|
|
333
|
+
const newParentOpts = {
|
|
334
|
+
sandboxHidden: ignoreParentHidden ? false : isHidden,
|
|
335
|
+
sandboxReadOnly: isReadOnly
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// set and inherit originalSandboxHidden if needed
|
|
339
|
+
if (
|
|
340
|
+
parentOpts?.originalSandboxHidden === true ||
|
|
341
|
+
(ignoreParentHidden && isHidden)
|
|
342
|
+
) {
|
|
343
|
+
newParentOpts.originalSandboxHidden = true
|
|
344
|
+
}
|
|
345
|
+
|
|
303
346
|
applyPropertiesConfig(context, innerConfig, {
|
|
304
347
|
original,
|
|
305
348
|
customProxies,
|
|
306
349
|
prop: innerKey,
|
|
307
350
|
isRoot: false,
|
|
308
351
|
isGrouped: true,
|
|
309
|
-
parentOpts:
|
|
352
|
+
parentOpts: newParentOpts
|
|
310
353
|
}, readOnlyConfigured)
|
|
311
354
|
})
|
|
312
355
|
}
|
|
313
356
|
|
|
314
357
|
if (isHidden) {
|
|
315
|
-
|
|
358
|
+
if (ignoreParentHidden) {
|
|
359
|
+
// when parent is hidden but there are configuration for child properties we just copy
|
|
360
|
+
// the properties listed there, we do this because we want to work with just the configured
|
|
361
|
+
// properties, the other properties should not be exposed
|
|
362
|
+
const currentValue = get(context, prop)
|
|
363
|
+
|
|
364
|
+
if (currentValue != null && typeof currentValue === 'object') {
|
|
365
|
+
let newValue
|
|
366
|
+
|
|
367
|
+
if (Array.isArray(currentValue)) {
|
|
368
|
+
newValue = []
|
|
369
|
+
} else {
|
|
370
|
+
newValue = {}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const config of [hierarchyPropertiesConfig.standalone, hierarchyPropertiesConfig.inner]) {
|
|
374
|
+
if (config == null) {
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const childProp of Object.keys(config)) {
|
|
379
|
+
if (hasOwn(context, childProp)) {
|
|
380
|
+
const childValue = get(context, childProp)
|
|
381
|
+
const targetProp = childProp.replace(`${prop}.`, '')
|
|
382
|
+
set(newValue, targetProp, childValue)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
set(context, prop, newValue)
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
omitProp(context, prop)
|
|
391
|
+
}
|
|
316
392
|
} else if (isReadOnly) {
|
|
317
393
|
readOnlyProp(context, prop, readOnlyConfigured, customProxies, {
|
|
318
394
|
onlyTopLevel: false,
|
|
@@ -38,7 +38,7 @@ module.exports = function createSandboxRequire (safeExecution, isolateModules, m
|
|
|
38
38
|
|
|
39
39
|
if (isolateModules) {
|
|
40
40
|
const requireExtensions = Object.create(null)
|
|
41
|
-
isolatedRequire.setDefaultRequireExtensions(requireExtensions,
|
|
41
|
+
isolatedRequire.setDefaultRequireExtensions(requireExtensions, requireFromRootDirectory, compileScript)
|
|
42
42
|
modulesMeta.requireExtensions = requireExtensions
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -25,6 +25,9 @@ module.exports = function createRunInSandbox (reporter) {
|
|
|
25
25
|
// it may turn out it is a bad approach in assets so we gonna delete it here
|
|
26
26
|
const executionFnName = `${nanoid()}_executionFn`
|
|
27
27
|
|
|
28
|
+
// creating new id different than execution to ensure user code can not get access to
|
|
29
|
+
// internal functions by using the __sandboxId
|
|
30
|
+
context.__sandboxId = nanoid()
|
|
28
31
|
context[executionFnName] = executionFn
|
|
29
32
|
context.__appDirectory = reporter.options.appDirectory
|
|
30
33
|
context.__rootDirectory = reporter.options.rootDirectory
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsreport/jsreport-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"description": "javascript based business reporting",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"report",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@node-rs/jsonwebtoken": "0.2.0",
|
|
81
|
+
"jsdom": "17.0.0",
|
|
81
82
|
"mocha": "10.1.0",
|
|
82
83
|
"should": "13.2.3",
|
|
83
84
|
"standard": "16.0.4",
|