@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
- ### 4.2.1
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 WeakMap()
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(context)) {
220
- contextExecutionChainMap.set(context, [])
221
+ if (!contextExecutionChainMap.has(sandboxId)) {
222
+ contextExecutionChainMap.set(sandboxId, [])
221
223
  }
222
224
 
223
- contextExecutionChainMap.get(context).push(executionId)
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
- const clonedMap = new Map(asyncResultMap)
264
+ let clonedMap = new Map(asyncResultMap)
265
+
263
266
  while (clonedMap.size > 0) {
264
- await Promise.all([...clonedMap.keys()].map(async (k) => {
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(context, contextExecutionChainMap.get(context).filter((id) => id !== executionId))
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: { sandboxHidden: isHidden, sandboxReadOnly: isReadOnly }
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: { sandboxHidden: isHidden, sandboxReadOnly: isReadOnly }
352
+ parentOpts: newParentOpts
310
353
  }, readOnlyConfigured)
311
354
  })
312
355
  }
313
356
 
314
357
  if (isHidden) {
315
- omitProp(context, prop)
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, modulesCache, compileScript)
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.2.2",
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",