@xh/hoist 77.0.0-SNAPSHOT.1761257771095 → 77.0.0-SNAPSHOT.1761672695220
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/CHANGELOG.md +21 -0
- package/admin/jsonsearch/impl/JsonSearchImplModel.ts +1 -1
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +1 -1
- package/admin/tabs/cluster/instances/logs/LogDisplayModel.ts +1 -1
- package/admin/tabs/cluster/instances/logs/levels/LogLevelDialogModel.ts +1 -1
- package/admin/tabs/general/config/ConfigPanelModel.ts +1 -1
- package/admin/tabs/monitor/editor/MonitorEditorDialog.ts +1 -1
- package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +1 -1
- package/admin/tabs/userData/prefs/UserPreferenceModel.ts +1 -1
- package/admin/tabs/userData/prefs/editor/PrefEditorModel.ts +1 -1
- package/admin/tabs/userData/users/UserModel.ts +1 -0
- package/build/types/core/AppSpec.d.ts +14 -7
- package/build/types/data/Field.d.ts +18 -9
- package/build/types/data/Store.d.ts +2 -1
- package/build/types/data/cube/Query.d.ts +1 -1
- package/build/types/data/cube/ViewRowData.d.ts +2 -0
- package/cmp/chart/impl/copyToClipboard.ts +14 -8
- package/cmp/treemap/TreeMap.ts +4 -14
- package/cmp/treemap/TreeMapModel.ts +2 -2
- package/core/AppSpec.ts +14 -7
- package/data/Field.ts +24 -20
- package/data/Store.ts +21 -8
- package/data/cube/Query.ts +1 -1
- package/data/cube/ViewRowData.ts +3 -0
- package/data/cube/row/AggregateRow.ts +1 -0
- package/data/cube/row/BucketRow.ts +1 -0
- package/data/cube/row/LeafRow.ts +1 -1
- package/format/FormatNumber.ts +2 -2
- package/kit/highcharts/index.ts +2 -2
- package/package.json +1 -1
- package/promise/Promise.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## 77.0.0-SNAPSHOT - unreleased
|
|
4
4
|
|
|
5
|
+
### 💥 Breaking Changes
|
|
6
|
+
|
|
7
|
+
* The `disableXssProtection` flag supported by `AppSpec` and `FieldSpec` has been removed and
|
|
8
|
+
replaced with its opposite, `enableXssProtection`, now an opt-in feature.
|
|
9
|
+
* While store-based XSS protection via DomPurify is still available to apps that can display
|
|
10
|
+
untrusted or potentially malicious data, this is an uncommon use case for Hoist apps and was
|
|
11
|
+
deemed to not provide enough benefit relative to potential performance pitfalls for most
|
|
12
|
+
applications. In addition, the core change to React-based AG Grid rendering has reduced the
|
|
13
|
+
attack surface for such exploits relative to when this system was first implemented.
|
|
14
|
+
* Apps that were previously opting-out via `disableXssProtection` should simply remove that
|
|
15
|
+
flag. Apps for which this protection remains important should enable at either the app level
|
|
16
|
+
or for selected Fields and/or Stores.
|
|
17
|
+
|
|
18
|
+
## 76.2.0 - 2025-10-22
|
|
19
|
+
|
|
20
|
+
### ⚙️ Technical
|
|
21
|
+
|
|
22
|
+
* Performance improvements to Store for large data sets.
|
|
23
|
+
* New property `cubeRowType` on `ViewRowData` supports identifying bucketed rows.
|
|
24
|
+
* `waitFor` can accept a null value for a timeout.
|
|
25
|
+
|
|
5
26
|
## 76.1.0 - 2025-10-17
|
|
6
27
|
|
|
7
28
|
### 🎁 New Features
|
|
@@ -395,7 +395,7 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
|
|
|
395
395
|
treeStyle: TreeStyle.HIGHLIGHTS_AND_BORDERS,
|
|
396
396
|
autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
|
|
397
397
|
exportOptions: {filename: exportFilename('activity-summary')},
|
|
398
|
-
emptyText: 'No activity reported
|
|
398
|
+
emptyText: 'No activity reported.',
|
|
399
399
|
sortBy: ['cubeLabel'],
|
|
400
400
|
expandLevel: 1,
|
|
401
401
|
levelLabels: () => ['Total', ...this.groupingChooserModel.valueDisplayNames],
|
|
@@ -136,7 +136,7 @@ export class LogDisplayModel extends HoistModel {
|
|
|
136
136
|
hideHeaders: true,
|
|
137
137
|
rowBorders: false,
|
|
138
138
|
sizingMode: 'tiny',
|
|
139
|
-
emptyText: 'No log entries found
|
|
139
|
+
emptyText: 'No log entries found.',
|
|
140
140
|
sortBy: 'rowNum|asc',
|
|
141
141
|
autosizeOptions: {mode: 'disabled'},
|
|
142
142
|
store: {
|
|
@@ -57,7 +57,7 @@ export class ConfigPanelModel extends HoistModel {
|
|
|
57
57
|
store: new RestStore({
|
|
58
58
|
url: 'rest/configAdmin',
|
|
59
59
|
reloadLookupsOnLoad: true,
|
|
60
|
-
fieldDefaults: {
|
|
60
|
+
fieldDefaults: {enableXssProtection: false},
|
|
61
61
|
fields: [
|
|
62
62
|
{...(Col.name.field as FieldSpec), required},
|
|
63
63
|
{
|
|
@@ -51,7 +51,7 @@ const modelSpec: RestGridConfig = {
|
|
|
51
51
|
showRefreshButton: true,
|
|
52
52
|
store: {
|
|
53
53
|
url: 'rest/monitorAdmin',
|
|
54
|
-
fieldDefaults: {
|
|
54
|
+
fieldDefaults: {enableXssProtection: false},
|
|
55
55
|
fields: [
|
|
56
56
|
{...(MCol.code.field as FieldSpec), required},
|
|
57
57
|
MCol.metricUnit.field,
|
|
@@ -51,7 +51,7 @@ export class JsonBlobModel extends HoistModel {
|
|
|
51
51
|
store: {
|
|
52
52
|
url: 'rest/jsonBlobAdmin',
|
|
53
53
|
reloadLookupsOnLoad: true,
|
|
54
|
-
fieldDefaults: {
|
|
54
|
+
fieldDefaults: {enableXssProtection: false},
|
|
55
55
|
fields: [
|
|
56
56
|
{...(JBCol.token.field as FieldSpec), editable: false},
|
|
57
57
|
JBCol.owner.field,
|
|
@@ -34,7 +34,7 @@ export class UserPreferenceModel extends HoistModel {
|
|
|
34
34
|
store: {
|
|
35
35
|
url: 'rest/userPreferenceAdmin',
|
|
36
36
|
reloadLookupsOnLoad: true,
|
|
37
|
-
fieldDefaults: {
|
|
37
|
+
fieldDefaults: {enableXssProtection: false},
|
|
38
38
|
fields: [
|
|
39
39
|
{
|
|
40
40
|
...(Col.name.field as FieldSpec),
|
|
@@ -47,7 +47,7 @@ export class PrefEditorModel extends HoistModel {
|
|
|
47
47
|
store: {
|
|
48
48
|
url: 'rest/preferenceAdmin',
|
|
49
49
|
reloadLookupsOnLoad: true,
|
|
50
|
-
fieldDefaults: {
|
|
50
|
+
fieldDefaults: {enableXssProtection: false},
|
|
51
51
|
fields: [
|
|
52
52
|
{...(Col.name.field as FieldSpec), required},
|
|
53
53
|
{
|
|
@@ -58,12 +58,19 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
58
58
|
*/
|
|
59
59
|
disableWebSockets?: boolean;
|
|
60
60
|
/**
|
|
61
|
-
* True to
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* True to enable Field-level XSS protection by default across all Stores/Fields in the app.
|
|
62
|
+
* Available as an extra precaution for use with apps that might display arbitrary input from
|
|
63
|
+
* untrusted or external users. This feature does exact a minor performance penalty during data
|
|
64
|
+
* parsing, which can be significant in aggregate for very large stores containing records with
|
|
65
|
+
* many `string` fields.
|
|
66
|
+
*
|
|
67
|
+
* Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
|
|
68
|
+
* Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
|
|
69
|
+
* building secured internal apps with large datasets and tight performance tolerances.
|
|
70
|
+
*
|
|
71
|
+
* @see FieldSpec.enableXssProtection
|
|
65
72
|
*/
|
|
66
|
-
|
|
73
|
+
enableXssProtection?: boolean;
|
|
67
74
|
/**
|
|
68
75
|
* True to show a login form on initialization when not authenticated. Default is `false` as
|
|
69
76
|
* most Hoist applications are expected to use OAuth or SSO for authn.
|
|
@@ -111,7 +118,7 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
111
118
|
trackAppLoad?: boolean;
|
|
112
119
|
/** @deprecated - use {@link AppSpec.disableWebSockets} instead. */
|
|
113
120
|
webSocketsEnabled?: boolean;
|
|
114
|
-
constructor({ authModelClass, checkAccess, clientAppCode, clientAppName, componentClass, containerClass, disableWebSockets,
|
|
121
|
+
constructor({ authModelClass, checkAccess, clientAppCode, clientAppName, componentClass, containerClass, disableWebSockets, enableXssProtection, enableLoginForm, enableLogout, idlePanel, isMobileApp, lockoutMessage, lockoutPanel, loginMessage, modelClass, showBrowserContextMenu, trackAppLoad, webSocketsEnabled }: {
|
|
115
122
|
authModelClass?: typeof HoistAuthModel;
|
|
116
123
|
checkAccess: any;
|
|
117
124
|
clientAppCode?: string;
|
|
@@ -119,7 +126,7 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
119
126
|
componentClass: any;
|
|
120
127
|
containerClass: any;
|
|
121
128
|
disableWebSockets?: boolean;
|
|
122
|
-
|
|
129
|
+
enableXssProtection?: boolean;
|
|
123
130
|
enableLoginForm?: boolean;
|
|
124
131
|
enableLogout?: boolean;
|
|
125
132
|
idlePanel?: any;
|
|
@@ -17,15 +17,24 @@ export interface FieldSpec {
|
|
|
17
17
|
/** Rules to apply to this field. */
|
|
18
18
|
rules?: RuleLike[];
|
|
19
19
|
/**
|
|
20
|
-
* True to
|
|
21
|
-
*
|
|
20
|
+
* True to enable built-in XSS (cross-site scripting) protection to all incoming String values
|
|
21
|
+
* using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
|
|
22
22
|
*
|
|
23
23
|
* DOMPurify provides fast escaping of dangerous HTML, scripting, and other content that can be
|
|
24
24
|
* used to execute XSS attacks, while allowing common and expected HTML and style tags.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* This feature does exact a minor performance penalty during data parsing, which can be
|
|
27
|
+
* significant in aggregate for very large stores containing records with many `string` fields.
|
|
28
|
+
*
|
|
29
|
+
* For extra safety, apps which are open to potentially-untrusted users or display other
|
|
30
|
+
* potentially dangerous string content can opt into this setting app-wide via
|
|
31
|
+
* {@link AppSpec.enableXssProtection}. Field-level setting will override any app-level default.
|
|
32
|
+
*
|
|
33
|
+
* Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
|
|
34
|
+
* Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
|
|
35
|
+
* building secured internal apps with large datasets and tight performance tolerances.
|
|
27
36
|
*/
|
|
28
|
-
|
|
37
|
+
enableXssProtection?: boolean;
|
|
29
38
|
}
|
|
30
39
|
/** Metadata for an individual data field within a {@link StoreRecord}. */
|
|
31
40
|
export declare class Field {
|
|
@@ -35,8 +44,8 @@ export declare class Field {
|
|
|
35
44
|
readonly displayName: string;
|
|
36
45
|
readonly defaultValue: any;
|
|
37
46
|
readonly rules: Rule[];
|
|
38
|
-
readonly
|
|
39
|
-
constructor({ name, type, displayName, defaultValue, rules,
|
|
47
|
+
readonly enableXssProtection: boolean;
|
|
48
|
+
constructor({ name, type, displayName, defaultValue, rules, enableXssProtection }: FieldSpec);
|
|
40
49
|
parseVal(val: any): any;
|
|
41
50
|
isEqual(val1: any, val2: any): boolean;
|
|
42
51
|
private processRuleSpecs;
|
|
@@ -46,11 +55,11 @@ export declare class Field {
|
|
|
46
55
|
* @param val - raw value to parse.
|
|
47
56
|
* @param type - data type of the field to use for possible conversion.
|
|
48
57
|
* @param defaultValue - typed value to return if `val` undefined or null.
|
|
49
|
-
* @param
|
|
50
|
-
*
|
|
58
|
+
* @param enableXssProtection - true to enable XSS (cross-site scripting) protection.
|
|
59
|
+
* See {@link FieldSpec.enableXssProtection} for additional details.
|
|
51
60
|
* @returns resulting value, potentially parsed or cast as per type.
|
|
52
61
|
*/
|
|
53
|
-
export declare function parseFieldValue(val: any, type: FieldType, defaultValue?: any,
|
|
62
|
+
export declare function parseFieldValue(val: any, type: FieldType, defaultValue?: any, enableXssProtection?: boolean): any;
|
|
54
63
|
/** Data types for Fields used within Hoist Store Records and Cubes. */
|
|
55
64
|
export declare const FieldType: Readonly<{
|
|
56
65
|
TAGS: "tags";
|
|
@@ -12,7 +12,7 @@ export interface StoreConfig {
|
|
|
12
12
|
* Default configs applied to `Field` instances constructed internally by this Store.
|
|
13
13
|
* @see FieldSpec
|
|
14
14
|
*/
|
|
15
|
-
fieldDefaults?:
|
|
15
|
+
fieldDefaults?: Omit<FieldSpec, 'name'>;
|
|
16
16
|
/**
|
|
17
17
|
* Specification for producing an immutable unique id for each record. May be provided as
|
|
18
18
|
* either a string property name (default is 'id') or a function that receives the raw data
|
|
@@ -401,6 +401,7 @@ export declare class Store extends HoistBase {
|
|
|
401
401
|
private rebuildFiltered;
|
|
402
402
|
private createRecord;
|
|
403
403
|
private createRecords;
|
|
404
|
+
private get summaryRecordIds();
|
|
404
405
|
private parseRaw;
|
|
405
406
|
private parseUpdate;
|
|
406
407
|
private createDataDefaults;
|
|
@@ -81,7 +81,7 @@ export interface QueryConfig {
|
|
|
81
81
|
*
|
|
82
82
|
* This can be used to break selected aggregations into sub-groups dynamically, without having
|
|
83
83
|
* to define another dimension in the Cube and have it apply to all aggregations. See the
|
|
84
|
-
* {@link BucketSpec} interface for additional information.
|
|
84
|
+
* {@link BucketSpecFn} type and {@link BucketSpec} interface for additional information.
|
|
85
85
|
*
|
|
86
86
|
* Defaults to {@link Cube.bucketSpecFn}.
|
|
87
87
|
*/
|
|
@@ -7,6 +7,8 @@ export declare class ViewRowData {
|
|
|
7
7
|
constructor(id: string);
|
|
8
8
|
/** Unique id. */
|
|
9
9
|
id: string;
|
|
10
|
+
/** Denotes a type for the row */
|
|
11
|
+
cubeRowType: 'leaf' | 'aggregate' | 'bucket';
|
|
10
12
|
/**
|
|
11
13
|
* Label of the row. The dimension value or, for leaf rows. the underlying cubeId.
|
|
12
14
|
* Suitable for display, although apps will typically wish to customize leaf row rendering.
|
|
@@ -24,14 +24,14 @@ export function installCopyToClipboard(Highcharts) {
|
|
|
24
24
|
try {
|
|
25
25
|
const blobPromise = convertChartToPngAsync(this),
|
|
26
26
|
clipboardItemInput = new window.ClipboardItem({
|
|
27
|
-
// Safari requires an unresolved promise.
|
|
27
|
+
// Safari requires an unresolved promise. See https://bugs.webkit.org/show_bug.cgi?id=222262 for discussion
|
|
28
28
|
'image/png': Highcharts.isSafari ? blobPromise : await blobPromise
|
|
29
29
|
});
|
|
30
30
|
await window.navigator.clipboard.write([clipboardItemInput]);
|
|
31
31
|
XH.successToast('Chart copied to clipboard');
|
|
32
32
|
} catch (e) {
|
|
33
33
|
XH.handleException(e, {showAlert: false, logOnServer: true});
|
|
34
|
-
XH.dangerToast('Error: Chart could not be copied.
|
|
34
|
+
XH.dangerToast('Error: Chart could not be copied. This error has been logged.');
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
});
|
|
@@ -41,8 +41,14 @@ export function installCopyToClipboard(Highcharts) {
|
|
|
41
41
|
// Implementation
|
|
42
42
|
//------------------
|
|
43
43
|
async function convertChartToPngAsync(chart) {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const svg = await new Promise((resolve, reject) =>
|
|
45
|
+
chart.getSVGForLocalExport(
|
|
46
|
+
chart.options.exporting,
|
|
47
|
+
{},
|
|
48
|
+
() => reject('Cannot fallback to export server'),
|
|
49
|
+
svg => resolve(svg)
|
|
50
|
+
)
|
|
51
|
+
),
|
|
46
52
|
svgUrl = svgToDataUrl(svg),
|
|
47
53
|
pngDataUrl = await svgUrlToPngDataUrlAsync(svgUrl),
|
|
48
54
|
ret = await loadBlob(pngDataUrl);
|
|
@@ -59,7 +65,7 @@ function memoryCleanup(svgUrl) {
|
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
/**
|
|
62
|
-
* Convert dataUri to blob
|
|
68
|
+
* Convert dataUri converted to blob
|
|
63
69
|
*/
|
|
64
70
|
async function loadBlob(dataUrl) {
|
|
65
71
|
const fetched = await fetch(dataUrl);
|
|
@@ -78,7 +84,7 @@ function svgToDataUrl(svg) {
|
|
|
78
84
|
try {
|
|
79
85
|
// Safari requires data URI since it doesn't allow navigation to blob
|
|
80
86
|
// URLs.
|
|
81
|
-
// foreignObjects
|
|
87
|
+
// foreignObjects dont work well in Blobs in Chrome (#14780).
|
|
82
88
|
if (!isWebKitButNotChrome && svg.indexOf('<foreignObject') === -1) {
|
|
83
89
|
return domurl.createObjectURL(
|
|
84
90
|
new window.Blob([svg], {
|
|
@@ -88,12 +94,12 @@ function svgToDataUrl(svg) {
|
|
|
88
94
|
}
|
|
89
95
|
} catch (e) {}
|
|
90
96
|
|
|
91
|
-
//
|
|
97
|
+
// safari, firefox, or svgs with foreignObect returns this
|
|
92
98
|
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
/**
|
|
96
|
-
* Get PNG data:URL from image URL.
|
|
102
|
+
* Get PNG data:URL from image URL. Pass in callbacks to handle results.
|
|
97
103
|
*/
|
|
98
104
|
async function svgUrlToPngDataUrlAsync(imageURL, scale = 1) {
|
|
99
105
|
const img = new window.Image(),
|
package/cmp/treemap/TreeMap.ts
CHANGED
|
@@ -172,6 +172,8 @@ class TreeMapLocalModel extends HoistModel {
|
|
|
172
172
|
this.prevConfig = cloneDeep(chartCfg);
|
|
173
173
|
this.createChart(config);
|
|
174
174
|
}
|
|
175
|
+
|
|
176
|
+
this.updateLabelVisibility();
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
createChart(config) {
|
|
@@ -193,25 +195,13 @@ class TreeMapLocalModel extends HoistModel {
|
|
|
193
195
|
|
|
194
196
|
assign(config.chart, parentDims, {renderTo: chartElem});
|
|
195
197
|
this.withDebug(['Creating new TreeMap', `${newData.length} records`], () => {
|
|
196
|
-
this.chart = Highcharts.chart(config
|
|
197
|
-
this.updateLabelVisibility();
|
|
198
|
-
});
|
|
198
|
+
this.chart = Highcharts.chart(config);
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
@logWithDebug
|
|
203
203
|
reloadSeriesData(newData) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
this.chart.series[0].setData(newData, true, false);
|
|
207
|
-
|
|
208
|
-
// Use an event handler to trigger label updates
|
|
209
|
-
// This approach was required when `cluster` series option is enabled
|
|
210
|
-
const onRedraw = () => {
|
|
211
|
-
this.updateLabelVisibility();
|
|
212
|
-
Highcharts.removeEvent(this.chart, 'redraw', onRedraw);
|
|
213
|
-
};
|
|
214
|
-
Highcharts.addEvent(this.chart, 'redraw', onRedraw);
|
|
204
|
+
this.chart?.series[0].setData(newData, true, false);
|
|
215
205
|
}
|
|
216
206
|
|
|
217
207
|
startResize = ({width, height}) => {
|
|
@@ -465,7 +465,7 @@ export class TreeMapModel extends HoistModel {
|
|
|
465
465
|
//----------------------
|
|
466
466
|
defaultOnClick = (record, e) => {
|
|
467
467
|
const {gridModel} = this;
|
|
468
|
-
if (!gridModel
|
|
468
|
+
if (!gridModel) return;
|
|
469
469
|
|
|
470
470
|
// Select nodes in grid
|
|
471
471
|
const {selModel} = gridModel;
|
|
@@ -477,7 +477,7 @@ export class TreeMapModel extends HoistModel {
|
|
|
477
477
|
};
|
|
478
478
|
|
|
479
479
|
defaultOnDoubleClick = record => {
|
|
480
|
-
if (!this.gridModel?.treeMode || isEmpty(record
|
|
480
|
+
if (!this.gridModel?.treeMode || isEmpty(record.children)) return;
|
|
481
481
|
this.toggleNodeExpanded(record.treePath);
|
|
482
482
|
};
|
|
483
483
|
}
|
package/core/AppSpec.ts
CHANGED
|
@@ -71,12 +71,19 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
71
71
|
disableWebSockets?: boolean;
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* True to
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
74
|
+
* True to enable Field-level XSS protection by default across all Stores/Fields in the app.
|
|
75
|
+
* Available as an extra precaution for use with apps that might display arbitrary input from
|
|
76
|
+
* untrusted or external users. This feature does exact a minor performance penalty during data
|
|
77
|
+
* parsing, which can be significant in aggregate for very large stores containing records with
|
|
78
|
+
* many `string` fields.
|
|
79
|
+
*
|
|
80
|
+
* Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
|
|
81
|
+
* Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
|
|
82
|
+
* building secured internal apps with large datasets and tight performance tolerances.
|
|
83
|
+
*
|
|
84
|
+
* @see FieldSpec.enableXssProtection
|
|
78
85
|
*/
|
|
79
|
-
|
|
86
|
+
enableXssProtection?: boolean;
|
|
80
87
|
|
|
81
88
|
/**
|
|
82
89
|
* True to show a login form on initialization when not authenticated. Default is `false` as
|
|
@@ -144,7 +151,7 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
144
151
|
componentClass,
|
|
145
152
|
containerClass,
|
|
146
153
|
disableWebSockets = false,
|
|
147
|
-
|
|
154
|
+
enableXssProtection = false,
|
|
148
155
|
enableLoginForm = false,
|
|
149
156
|
enableLogout = false,
|
|
150
157
|
idlePanel = null,
|
|
@@ -191,7 +198,7 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
191
198
|
this.componentClass = componentClass;
|
|
192
199
|
this.containerClass = containerClass;
|
|
193
200
|
this.disableWebSockets = disableWebSockets;
|
|
194
|
-
this.
|
|
201
|
+
this.enableXssProtection = enableXssProtection;
|
|
195
202
|
this.enableLoginForm = enableLoginForm;
|
|
196
203
|
this.enableLogout = enableLogout;
|
|
197
204
|
this.idlePanel = idlePanel;
|
package/data/Field.ts
CHANGED
|
@@ -36,15 +36,24 @@ export interface FieldSpec {
|
|
|
36
36
|
rules?: RuleLike[];
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* True to
|
|
40
|
-
*
|
|
39
|
+
* True to enable built-in XSS (cross-site scripting) protection to all incoming String values
|
|
40
|
+
* using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
|
|
41
41
|
*
|
|
42
42
|
* DOMPurify provides fast escaping of dangerous HTML, scripting, and other content that can be
|
|
43
43
|
* used to execute XSS attacks, while allowing common and expected HTML and style tags.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
45
|
+
* This feature does exact a minor performance penalty during data parsing, which can be
|
|
46
|
+
* significant in aggregate for very large stores containing records with many `string` fields.
|
|
47
|
+
*
|
|
48
|
+
* For extra safety, apps which are open to potentially-untrusted users or display other
|
|
49
|
+
* potentially dangerous string content can opt into this setting app-wide via
|
|
50
|
+
* {@link AppSpec.enableXssProtection}. Field-level setting will override any app-level default.
|
|
51
|
+
*
|
|
52
|
+
* Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
|
|
53
|
+
* Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
|
|
54
|
+
* building secured internal apps with large datasets and tight performance tolerances.
|
|
46
55
|
*/
|
|
47
|
-
|
|
56
|
+
enableXssProtection?: boolean;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
/** Metadata for an individual data field within a {@link StoreRecord}. */
|
|
@@ -58,7 +67,7 @@ export class Field {
|
|
|
58
67
|
readonly displayName: string;
|
|
59
68
|
readonly defaultValue: any;
|
|
60
69
|
readonly rules: Rule[];
|
|
61
|
-
readonly
|
|
70
|
+
readonly enableXssProtection: boolean;
|
|
62
71
|
|
|
63
72
|
constructor({
|
|
64
73
|
name,
|
|
@@ -66,19 +75,19 @@ export class Field {
|
|
|
66
75
|
displayName,
|
|
67
76
|
defaultValue = null,
|
|
68
77
|
rules = [],
|
|
69
|
-
|
|
78
|
+
enableXssProtection = XH.appSpec.enableXssProtection
|
|
70
79
|
}: FieldSpec) {
|
|
71
80
|
this.name = name;
|
|
72
81
|
this.type = type;
|
|
73
82
|
this.displayName = withDefault(displayName, genDisplayName(name));
|
|
74
83
|
this.defaultValue = defaultValue;
|
|
75
84
|
this.rules = this.processRuleSpecs(rules);
|
|
76
|
-
this.
|
|
85
|
+
this.enableXssProtection = enableXssProtection;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
parseVal(val: any): any {
|
|
80
|
-
const {type, defaultValue,
|
|
81
|
-
return parseFieldValue(val, type, defaultValue,
|
|
89
|
+
const {type, defaultValue, enableXssProtection} = this;
|
|
90
|
+
return parseFieldValue(val, type, defaultValue, enableXssProtection);
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
isEqual(val1: any, val2: any): boolean {
|
|
@@ -102,35 +111,30 @@ export class Field {
|
|
|
102
111
|
* @param val - raw value to parse.
|
|
103
112
|
* @param type - data type of the field to use for possible conversion.
|
|
104
113
|
* @param defaultValue - typed value to return if `val` undefined or null.
|
|
105
|
-
* @param
|
|
106
|
-
*
|
|
114
|
+
* @param enableXssProtection - true to enable XSS (cross-site scripting) protection.
|
|
115
|
+
* See {@link FieldSpec.enableXssProtection} for additional details.
|
|
107
116
|
* @returns resulting value, potentially parsed or cast as per type.
|
|
108
117
|
*/
|
|
109
118
|
export function parseFieldValue(
|
|
110
119
|
val: any,
|
|
111
120
|
type: FieldType,
|
|
112
121
|
defaultValue: any = null,
|
|
113
|
-
|
|
122
|
+
enableXssProtection: boolean = XH.appSpec.enableXssProtection
|
|
114
123
|
): any {
|
|
115
124
|
if (val === undefined || val === null) val = defaultValue;
|
|
116
125
|
if (val === null) return val;
|
|
117
126
|
|
|
118
|
-
const sanitizeValue = v => {
|
|
119
|
-
if (disableXssProtection || !isString(v)) return v;
|
|
120
|
-
return DOMPurify.sanitize(v);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
127
|
switch (type) {
|
|
124
128
|
case 'tags':
|
|
125
129
|
val = castArray(val);
|
|
126
130
|
val = val.map(v => {
|
|
127
|
-
v =
|
|
131
|
+
v = !enableXssProtection || !isString(v) ? v : DOMPurify.sanitize(v);
|
|
128
132
|
return v.toString();
|
|
129
133
|
});
|
|
130
134
|
return val;
|
|
131
135
|
case 'auto':
|
|
132
136
|
case 'json':
|
|
133
|
-
return
|
|
137
|
+
return !enableXssProtection || !isString(val) ? val : DOMPurify.sanitize(val);
|
|
134
138
|
case 'int':
|
|
135
139
|
val = toNumber(val);
|
|
136
140
|
return isFinite(val) ? Math.trunc(val) : null;
|
|
@@ -140,7 +144,7 @@ export function parseFieldValue(
|
|
|
140
144
|
return !!val;
|
|
141
145
|
case 'pwd':
|
|
142
146
|
case 'string':
|
|
143
|
-
val =
|
|
147
|
+
val = !enableXssProtection || !isString(val) ? val : DOMPurify.sanitize(val);
|
|
144
148
|
return val.toString();
|
|
145
149
|
case 'date':
|
|
146
150
|
return isDate(val) ? val : new Date(val);
|
package/data/Store.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface StoreConfig {
|
|
|
44
44
|
* Default configs applied to `Field` instances constructed internally by this Store.
|
|
45
45
|
* @see FieldSpec
|
|
46
46
|
*/
|
|
47
|
-
fieldDefaults?:
|
|
47
|
+
fieldDefaults?: Omit<FieldSpec, 'name'>;
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Specification for producing an immutable unique id for each record. May be provided as
|
|
@@ -978,17 +978,20 @@ export class Store extends HoistBase {
|
|
|
978
978
|
this.summaryRecords = null;
|
|
979
979
|
}
|
|
980
980
|
|
|
981
|
-
private parseFields(
|
|
981
|
+
private parseFields(
|
|
982
|
+
fields: Array<string | FieldSpec | Field>,
|
|
983
|
+
defaults: Omit<FieldSpec, 'name'>
|
|
984
|
+
): Field[] {
|
|
982
985
|
const ret = fields.map(f => {
|
|
983
986
|
if (f instanceof Field) return f;
|
|
984
987
|
|
|
985
|
-
|
|
988
|
+
let fieldSpec: FieldSpec = isString(f) ? {name: f} : f;
|
|
986
989
|
|
|
987
990
|
if (!isEmpty(defaults)) {
|
|
988
|
-
|
|
991
|
+
fieldSpec = defaultsDeep({}, fieldSpec, defaults);
|
|
989
992
|
}
|
|
990
993
|
|
|
991
|
-
return new this.defaultFieldClass(
|
|
994
|
+
return new this.defaultFieldClass(fieldSpec);
|
|
992
995
|
});
|
|
993
996
|
|
|
994
997
|
throwIf(
|
|
@@ -1041,26 +1044,36 @@ export class Store extends HoistBase {
|
|
|
1041
1044
|
return ret;
|
|
1042
1045
|
}
|
|
1043
1046
|
|
|
1044
|
-
private createRecords(
|
|
1047
|
+
private createRecords(
|
|
1048
|
+
rawData: PlainObject[],
|
|
1049
|
+
parent: StoreRecord,
|
|
1050
|
+
recordMap: Map<StoreRecordId, StoreRecord> = new Map(),
|
|
1051
|
+
summaryRecordIds: Set<StoreRecordId> = this.summaryRecordIds
|
|
1052
|
+
) {
|
|
1045
1053
|
const {loadTreeData, loadTreeDataFrom} = this;
|
|
1054
|
+
|
|
1046
1055
|
rawData.forEach(raw => {
|
|
1047
1056
|
const rec = this.createRecord(raw, parent),
|
|
1048
1057
|
{id} = rec;
|
|
1049
1058
|
|
|
1050
1059
|
throwIf(
|
|
1051
|
-
recordMap.has(id) ||
|
|
1060
|
+
recordMap.has(id) || summaryRecordIds.has(id),
|
|
1052
1061
|
`ID ${id} is not unique. Use the 'Store.idSpec' config to resolve a unique ID for each record.`
|
|
1053
1062
|
);
|
|
1054
1063
|
|
|
1055
1064
|
recordMap.set(id, rec);
|
|
1056
1065
|
|
|
1057
1066
|
if (loadTreeData && raw[loadTreeDataFrom]) {
|
|
1058
|
-
this.createRecords(raw[loadTreeDataFrom], rec, recordMap);
|
|
1067
|
+
this.createRecords(raw[loadTreeDataFrom], rec, recordMap, summaryRecordIds);
|
|
1059
1068
|
}
|
|
1060
1069
|
});
|
|
1061
1070
|
return recordMap;
|
|
1062
1071
|
}
|
|
1063
1072
|
|
|
1073
|
+
private get summaryRecordIds(): Set<StoreRecordId> {
|
|
1074
|
+
return new Set(this.summaryRecords?.map(it => it.id) ?? []);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1064
1077
|
private parseRaw(data: PlainObject): PlainObject {
|
|
1065
1078
|
// a) create/prepare the data object
|
|
1066
1079
|
const ret = Object.create(this._dataDefaults);
|
package/data/cube/Query.ts
CHANGED
|
@@ -109,7 +109,7 @@ export interface QueryConfig {
|
|
|
109
109
|
*
|
|
110
110
|
* This can be used to break selected aggregations into sub-groups dynamically, without having
|
|
111
111
|
* to define another dimension in the Cube and have it apply to all aggregations. See the
|
|
112
|
-
* {@link BucketSpec} interface for additional information.
|
|
112
|
+
* {@link BucketSpecFn} type and {@link BucketSpec} interface for additional information.
|
|
113
113
|
*
|
|
114
114
|
* Defaults to {@link Cube.bucketSpecFn}.
|
|
115
115
|
*/
|
package/data/cube/ViewRowData.ts
CHANGED
|
@@ -19,6 +19,9 @@ export class ViewRowData {
|
|
|
19
19
|
/** Unique id. */
|
|
20
20
|
id: string;
|
|
21
21
|
|
|
22
|
+
/** Denotes a type for the row */
|
|
23
|
+
cubeRowType: 'leaf' | 'aggregate' | 'bucket';
|
|
24
|
+
|
|
22
25
|
/**
|
|
23
26
|
* Label of the row. The dimension value or, for leaf rows. the underlying cubeId.
|
|
24
27
|
* Suitable for display, although apps will typically wish to customize leaf row rendering.
|