@xh/hoist 77.0.0-SNAPSHOT.1761229417098 → 77.0.0-SNAPSHOT.1761257771095
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 +0 -21
- 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 +0 -1
- package/build/types/core/AppSpec.d.ts +7 -14
- package/build/types/data/Field.d.ts +9 -18
- package/build/types/data/Store.d.ts +1 -2
- package/build/types/data/cube/Query.d.ts +1 -1
- package/build/types/data/cube/ViewRowData.d.ts +0 -2
- package/cmp/chart/impl/copyToClipboard.ts +8 -14
- package/cmp/treemap/TreeMap.ts +14 -4
- package/cmp/treemap/TreeMapModel.ts +2 -2
- package/core/AppSpec.ts +7 -14
- package/data/Field.ts +20 -24
- package/data/Store.ts +8 -21
- package/data/cube/Query.ts +1 -1
- package/data/cube/ViewRowData.ts +0 -3
- package/data/cube/row/AggregateRow.ts +0 -1
- package/data/cube/row/BucketRow.ts +0 -1
- package/data/cube/row/LeafRow.ts +1 -1
- 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,27 +2,6 @@
|
|
|
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
|
-
|
|
26
5
|
## 76.1.0 - 2025-10-17
|
|
27
6
|
|
|
28
7
|
### 🎁 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: {disableXssProtection: true},
|
|
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: {disableXssProtection: true},
|
|
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: {disableXssProtection: true},
|
|
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: {disableXssProtection: true},
|
|
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: {disableXssProtection: true},
|
|
51
51
|
fields: [
|
|
52
52
|
{...(Col.name.field as FieldSpec), required},
|
|
53
53
|
{
|
|
@@ -58,19 +58,12 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
58
58
|
*/
|
|
59
59
|
disableWebSockets?: boolean;
|
|
60
60
|
/**
|
|
61
|
-
* True to
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
|
61
|
+
* True to disable Field-level XSS protection by default across all Stores/Fields in the app.
|
|
62
|
+
* For use with secure, internal apps that do not display arbitrary/external user input and
|
|
63
|
+
* have tight performance tolerances and/or load very large record sets.
|
|
64
|
+
* @see FieldSpec.disableXssProtection
|
|
72
65
|
*/
|
|
73
|
-
|
|
66
|
+
disableXssProtection?: boolean;
|
|
74
67
|
/**
|
|
75
68
|
* True to show a login form on initialization when not authenticated. Default is `false` as
|
|
76
69
|
* most Hoist applications are expected to use OAuth or SSO for authn.
|
|
@@ -118,7 +111,7 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
118
111
|
trackAppLoad?: boolean;
|
|
119
112
|
/** @deprecated - use {@link AppSpec.disableWebSockets} instead. */
|
|
120
113
|
webSocketsEnabled?: boolean;
|
|
121
|
-
constructor({ authModelClass, checkAccess, clientAppCode, clientAppName, componentClass, containerClass, disableWebSockets,
|
|
114
|
+
constructor({ authModelClass, checkAccess, clientAppCode, clientAppName, componentClass, containerClass, disableWebSockets, disableXssProtection, enableLoginForm, enableLogout, idlePanel, isMobileApp, lockoutMessage, lockoutPanel, loginMessage, modelClass, showBrowserContextMenu, trackAppLoad, webSocketsEnabled }: {
|
|
122
115
|
authModelClass?: typeof HoistAuthModel;
|
|
123
116
|
checkAccess: any;
|
|
124
117
|
clientAppCode?: string;
|
|
@@ -126,7 +119,7 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
126
119
|
componentClass: any;
|
|
127
120
|
containerClass: any;
|
|
128
121
|
disableWebSockets?: boolean;
|
|
129
|
-
|
|
122
|
+
disableXssProtection?: boolean;
|
|
130
123
|
enableLoginForm?: boolean;
|
|
131
124
|
enableLogout?: boolean;
|
|
132
125
|
idlePanel?: any;
|
|
@@ -17,24 +17,15 @@ export interface FieldSpec {
|
|
|
17
17
|
/** Rules to apply to this field. */
|
|
18
18
|
rules?: RuleLike[];
|
|
19
19
|
/**
|
|
20
|
-
* True to
|
|
21
|
-
* using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
|
|
20
|
+
* True to disable built-in XSS (cross-site scripting) protection, applied by default to all
|
|
21
|
+
* incoming String values 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
|
-
*
|
|
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.
|
|
26
|
+
* Please contact XH if you find yourself needing to disable this protection!
|
|
36
27
|
*/
|
|
37
|
-
|
|
28
|
+
disableXssProtection?: boolean;
|
|
38
29
|
}
|
|
39
30
|
/** Metadata for an individual data field within a {@link StoreRecord}. */
|
|
40
31
|
export declare class Field {
|
|
@@ -44,8 +35,8 @@ export declare class Field {
|
|
|
44
35
|
readonly displayName: string;
|
|
45
36
|
readonly defaultValue: any;
|
|
46
37
|
readonly rules: Rule[];
|
|
47
|
-
readonly
|
|
48
|
-
constructor({ name, type, displayName, defaultValue, rules,
|
|
38
|
+
readonly disableXssProtection: boolean;
|
|
39
|
+
constructor({ name, type, displayName, defaultValue, rules, disableXssProtection }: FieldSpec);
|
|
49
40
|
parseVal(val: any): any;
|
|
50
41
|
isEqual(val1: any, val2: any): boolean;
|
|
51
42
|
private processRuleSpecs;
|
|
@@ -55,11 +46,11 @@ export declare class Field {
|
|
|
55
46
|
* @param val - raw value to parse.
|
|
56
47
|
* @param type - data type of the field to use for possible conversion.
|
|
57
48
|
* @param defaultValue - typed value to return if `val` undefined or null.
|
|
58
|
-
* @param
|
|
59
|
-
*
|
|
49
|
+
* @param disableXssProtection - true to disable XSS (cross-site scripting) protection.
|
|
50
|
+
* @see {@link FieldConfig} docs for additional details.
|
|
60
51
|
* @returns resulting value, potentially parsed or cast as per type.
|
|
61
52
|
*/
|
|
62
|
-
export declare function parseFieldValue(val: any, type: FieldType, defaultValue?: any,
|
|
53
|
+
export declare function parseFieldValue(val: any, type: FieldType, defaultValue?: any, disableXssProtection?: boolean): any;
|
|
63
54
|
/** Data types for Fields used within Hoist Store Records and Cubes. */
|
|
64
55
|
export declare const FieldType: Readonly<{
|
|
65
56
|
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?: any;
|
|
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,7 +401,6 @@ export declare class Store extends HoistBase {
|
|
|
401
401
|
private rebuildFiltered;
|
|
402
402
|
private createRecord;
|
|
403
403
|
private createRecords;
|
|
404
|
-
private get summaryRecordIds();
|
|
405
404
|
private parseRaw;
|
|
406
405
|
private parseUpdate;
|
|
407
406
|
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
|
|
84
|
+
* {@link BucketSpec} interface for additional information.
|
|
85
85
|
*
|
|
86
86
|
* Defaults to {@link Cube.bucketSpecFn}.
|
|
87
87
|
*/
|
|
@@ -7,8 +7,6 @@ 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';
|
|
12
10
|
/**
|
|
13
11
|
* Label of the row. The dimension value or, for leaf rows. the underlying cubeId.
|
|
14
12
|
* 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,14 +41,8 @@ export function installCopyToClipboard(Highcharts) {
|
|
|
41
41
|
// Implementation
|
|
42
42
|
//------------------
|
|
43
43
|
async function convertChartToPngAsync(chart) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
chart.options.exporting,
|
|
47
|
-
{},
|
|
48
|
-
() => reject('Cannot fallback to export server'),
|
|
49
|
-
svg => resolve(svg)
|
|
50
|
-
)
|
|
51
|
-
),
|
|
44
|
+
// v12 replacement for getSVGForLocalExport
|
|
45
|
+
const svg = chart.getSVG(),
|
|
52
46
|
svgUrl = svgToDataUrl(svg),
|
|
53
47
|
pngDataUrl = await svgUrlToPngDataUrlAsync(svgUrl),
|
|
54
48
|
ret = await loadBlob(pngDataUrl);
|
|
@@ -65,7 +59,7 @@ function memoryCleanup(svgUrl) {
|
|
|
65
59
|
}
|
|
66
60
|
|
|
67
61
|
/**
|
|
68
|
-
* Convert dataUri
|
|
62
|
+
* Convert dataUri to blob
|
|
69
63
|
*/
|
|
70
64
|
async function loadBlob(dataUrl) {
|
|
71
65
|
const fetched = await fetch(dataUrl);
|
|
@@ -84,7 +78,7 @@ function svgToDataUrl(svg) {
|
|
|
84
78
|
try {
|
|
85
79
|
// Safari requires data URI since it doesn't allow navigation to blob
|
|
86
80
|
// URLs.
|
|
87
|
-
// foreignObjects
|
|
81
|
+
// foreignObjects don't work well in Blobs in Chrome (#14780).
|
|
88
82
|
if (!isWebKitButNotChrome && svg.indexOf('<foreignObject') === -1) {
|
|
89
83
|
return domurl.createObjectURL(
|
|
90
84
|
new window.Blob([svg], {
|
|
@@ -94,12 +88,12 @@ function svgToDataUrl(svg) {
|
|
|
94
88
|
}
|
|
95
89
|
} catch (e) {}
|
|
96
90
|
|
|
97
|
-
//
|
|
91
|
+
// Safari, Firefox, or SVGs with foreignObject fallback
|
|
98
92
|
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
|
|
99
93
|
}
|
|
100
94
|
|
|
101
95
|
/**
|
|
102
|
-
* Get PNG data:URL from image URL.
|
|
96
|
+
* Get PNG data:URL from image URL.
|
|
103
97
|
*/
|
|
104
98
|
async function svgUrlToPngDataUrlAsync(imageURL, scale = 1) {
|
|
105
99
|
const img = new window.Image(),
|
package/cmp/treemap/TreeMap.ts
CHANGED
|
@@ -172,8 +172,6 @@ class TreeMapLocalModel extends HoistModel {
|
|
|
172
172
|
this.prevConfig = cloneDeep(chartCfg);
|
|
173
173
|
this.createChart(config);
|
|
174
174
|
}
|
|
175
|
-
|
|
176
|
-
this.updateLabelVisibility();
|
|
177
175
|
}
|
|
178
176
|
|
|
179
177
|
createChart(config) {
|
|
@@ -195,13 +193,25 @@ class TreeMapLocalModel extends HoistModel {
|
|
|
195
193
|
|
|
196
194
|
assign(config.chart, parentDims, {renderTo: chartElem});
|
|
197
195
|
this.withDebug(['Creating new TreeMap', `${newData.length} records`], () => {
|
|
198
|
-
this.chart = Highcharts.chart(config)
|
|
196
|
+
this.chart = Highcharts.chart(config, () => {
|
|
197
|
+
this.updateLabelVisibility();
|
|
198
|
+
});
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
@logWithDebug
|
|
203
203
|
reloadSeriesData(newData) {
|
|
204
|
-
this.chart
|
|
204
|
+
if (!this.chart) return;
|
|
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);
|
|
205
215
|
}
|
|
206
216
|
|
|
207
217
|
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) return;
|
|
468
|
+
if (!gridModel || !record) 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,19 +71,12 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
71
71
|
disableWebSockets?: boolean;
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* True to
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
|
74
|
+
* True to disable Field-level XSS protection by default across all Stores/Fields in the app.
|
|
75
|
+
* For use with secure, internal apps that do not display arbitrary/external user input and
|
|
76
|
+
* have tight performance tolerances and/or load very large record sets.
|
|
77
|
+
* @see FieldSpec.disableXssProtection
|
|
85
78
|
*/
|
|
86
|
-
|
|
79
|
+
disableXssProtection?: boolean;
|
|
87
80
|
|
|
88
81
|
/**
|
|
89
82
|
* True to show a login form on initialization when not authenticated. Default is `false` as
|
|
@@ -151,7 +144,7 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
151
144
|
componentClass,
|
|
152
145
|
containerClass,
|
|
153
146
|
disableWebSockets = false,
|
|
154
|
-
|
|
147
|
+
disableXssProtection = false,
|
|
155
148
|
enableLoginForm = false,
|
|
156
149
|
enableLogout = false,
|
|
157
150
|
idlePanel = null,
|
|
@@ -198,7 +191,7 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
|
|
|
198
191
|
this.componentClass = componentClass;
|
|
199
192
|
this.containerClass = containerClass;
|
|
200
193
|
this.disableWebSockets = disableWebSockets;
|
|
201
|
-
this.
|
|
194
|
+
this.disableXssProtection = disableXssProtection;
|
|
202
195
|
this.enableLoginForm = enableLoginForm;
|
|
203
196
|
this.enableLogout = enableLogout;
|
|
204
197
|
this.idlePanel = idlePanel;
|
package/data/Field.ts
CHANGED
|
@@ -36,24 +36,15 @@ export interface FieldSpec {
|
|
|
36
36
|
rules?: RuleLike[];
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* True to
|
|
40
|
-
* using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
|
|
39
|
+
* True to disable built-in XSS (cross-site scripting) protection, applied by default to all
|
|
40
|
+
* incoming String values 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
|
-
*
|
|
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.
|
|
45
|
+
* Please contact XH if you find yourself needing to disable this protection!
|
|
55
46
|
*/
|
|
56
|
-
|
|
47
|
+
disableXssProtection?: boolean;
|
|
57
48
|
}
|
|
58
49
|
|
|
59
50
|
/** Metadata for an individual data field within a {@link StoreRecord}. */
|
|
@@ -67,7 +58,7 @@ export class Field {
|
|
|
67
58
|
readonly displayName: string;
|
|
68
59
|
readonly defaultValue: any;
|
|
69
60
|
readonly rules: Rule[];
|
|
70
|
-
readonly
|
|
61
|
+
readonly disableXssProtection: boolean;
|
|
71
62
|
|
|
72
63
|
constructor({
|
|
73
64
|
name,
|
|
@@ -75,19 +66,19 @@ export class Field {
|
|
|
75
66
|
displayName,
|
|
76
67
|
defaultValue = null,
|
|
77
68
|
rules = [],
|
|
78
|
-
|
|
69
|
+
disableXssProtection = XH.appSpec.disableXssProtection
|
|
79
70
|
}: FieldSpec) {
|
|
80
71
|
this.name = name;
|
|
81
72
|
this.type = type;
|
|
82
73
|
this.displayName = withDefault(displayName, genDisplayName(name));
|
|
83
74
|
this.defaultValue = defaultValue;
|
|
84
75
|
this.rules = this.processRuleSpecs(rules);
|
|
85
|
-
this.
|
|
76
|
+
this.disableXssProtection = disableXssProtection;
|
|
86
77
|
}
|
|
87
78
|
|
|
88
79
|
parseVal(val: any): any {
|
|
89
|
-
const {type, defaultValue,
|
|
90
|
-
return parseFieldValue(val, type, defaultValue,
|
|
80
|
+
const {type, defaultValue, disableXssProtection} = this;
|
|
81
|
+
return parseFieldValue(val, type, defaultValue, disableXssProtection);
|
|
91
82
|
}
|
|
92
83
|
|
|
93
84
|
isEqual(val1: any, val2: any): boolean {
|
|
@@ -111,30 +102,35 @@ export class Field {
|
|
|
111
102
|
* @param val - raw value to parse.
|
|
112
103
|
* @param type - data type of the field to use for possible conversion.
|
|
113
104
|
* @param defaultValue - typed value to return if `val` undefined or null.
|
|
114
|
-
* @param
|
|
115
|
-
*
|
|
105
|
+
* @param disableXssProtection - true to disable XSS (cross-site scripting) protection.
|
|
106
|
+
* @see {@link FieldConfig} docs for additional details.
|
|
116
107
|
* @returns resulting value, potentially parsed or cast as per type.
|
|
117
108
|
*/
|
|
118
109
|
export function parseFieldValue(
|
|
119
110
|
val: any,
|
|
120
111
|
type: FieldType,
|
|
121
112
|
defaultValue: any = null,
|
|
122
|
-
|
|
113
|
+
disableXssProtection = XH.appSpec.disableXssProtection
|
|
123
114
|
): any {
|
|
124
115
|
if (val === undefined || val === null) val = defaultValue;
|
|
125
116
|
if (val === null) return val;
|
|
126
117
|
|
|
118
|
+
const sanitizeValue = v => {
|
|
119
|
+
if (disableXssProtection || !isString(v)) return v;
|
|
120
|
+
return DOMPurify.sanitize(v);
|
|
121
|
+
};
|
|
122
|
+
|
|
127
123
|
switch (type) {
|
|
128
124
|
case 'tags':
|
|
129
125
|
val = castArray(val);
|
|
130
126
|
val = val.map(v => {
|
|
131
|
-
v =
|
|
127
|
+
v = sanitizeValue(v);
|
|
132
128
|
return v.toString();
|
|
133
129
|
});
|
|
134
130
|
return val;
|
|
135
131
|
case 'auto':
|
|
136
132
|
case 'json':
|
|
137
|
-
return
|
|
133
|
+
return sanitizeValue(val);
|
|
138
134
|
case 'int':
|
|
139
135
|
val = toNumber(val);
|
|
140
136
|
return isFinite(val) ? Math.trunc(val) : null;
|
|
@@ -144,7 +140,7 @@ export function parseFieldValue(
|
|
|
144
140
|
return !!val;
|
|
145
141
|
case 'pwd':
|
|
146
142
|
case 'string':
|
|
147
|
-
val =
|
|
143
|
+
val = sanitizeValue(val);
|
|
148
144
|
return val.toString();
|
|
149
145
|
case 'date':
|
|
150
146
|
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?: any;
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Specification for producing an immutable unique id for each record. May be provided as
|
|
@@ -978,20 +978,17 @@ export class Store extends HoistBase {
|
|
|
978
978
|
this.summaryRecords = null;
|
|
979
979
|
}
|
|
980
980
|
|
|
981
|
-
private parseFields(
|
|
982
|
-
fields: Array<string | FieldSpec | Field>,
|
|
983
|
-
defaults: Omit<FieldSpec, 'name'>
|
|
984
|
-
): Field[] {
|
|
981
|
+
private parseFields(fields: any[], defaults: any): Field[] {
|
|
985
982
|
const ret = fields.map(f => {
|
|
986
983
|
if (f instanceof Field) return f;
|
|
987
984
|
|
|
988
|
-
|
|
985
|
+
if (isString(f)) f = {name: f};
|
|
989
986
|
|
|
990
987
|
if (!isEmpty(defaults)) {
|
|
991
|
-
|
|
988
|
+
f = defaultsDeep({}, f, defaults);
|
|
992
989
|
}
|
|
993
990
|
|
|
994
|
-
return new this.defaultFieldClass(
|
|
991
|
+
return new this.defaultFieldClass(f);
|
|
995
992
|
});
|
|
996
993
|
|
|
997
994
|
throwIf(
|
|
@@ -1044,36 +1041,26 @@ export class Store extends HoistBase {
|
|
|
1044
1041
|
return ret;
|
|
1045
1042
|
}
|
|
1046
1043
|
|
|
1047
|
-
private createRecords(
|
|
1048
|
-
rawData: PlainObject[],
|
|
1049
|
-
parent: StoreRecord,
|
|
1050
|
-
recordMap: Map<StoreRecordId, StoreRecord> = new Map(),
|
|
1051
|
-
summaryRecordIds: Set<StoreRecordId> = this.summaryRecordIds
|
|
1052
|
-
) {
|
|
1044
|
+
private createRecords(rawData: PlainObject[], parent: StoreRecord, recordMap = new Map()) {
|
|
1053
1045
|
const {loadTreeData, loadTreeDataFrom} = this;
|
|
1054
|
-
|
|
1055
1046
|
rawData.forEach(raw => {
|
|
1056
1047
|
const rec = this.createRecord(raw, parent),
|
|
1057
1048
|
{id} = rec;
|
|
1058
1049
|
|
|
1059
1050
|
throwIf(
|
|
1060
|
-
recordMap.has(id) ||
|
|
1051
|
+
recordMap.has(id) || this.summaryRecords?.some(it => it.id === id),
|
|
1061
1052
|
`ID ${id} is not unique. Use the 'Store.idSpec' config to resolve a unique ID for each record.`
|
|
1062
1053
|
);
|
|
1063
1054
|
|
|
1064
1055
|
recordMap.set(id, rec);
|
|
1065
1056
|
|
|
1066
1057
|
if (loadTreeData && raw[loadTreeDataFrom]) {
|
|
1067
|
-
this.createRecords(raw[loadTreeDataFrom], rec, recordMap
|
|
1058
|
+
this.createRecords(raw[loadTreeDataFrom], rec, recordMap);
|
|
1068
1059
|
}
|
|
1069
1060
|
});
|
|
1070
1061
|
return recordMap;
|
|
1071
1062
|
}
|
|
1072
1063
|
|
|
1073
|
-
private get summaryRecordIds(): Set<StoreRecordId> {
|
|
1074
|
-
return new Set(this.summaryRecords?.map(it => it.id) ?? []);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
1064
|
private parseRaw(data: PlainObject): PlainObject {
|
|
1078
1065
|
// a) create/prepare the data object
|
|
1079
1066
|
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
|
|
112
|
+
* {@link BucketSpec} interface for additional information.
|
|
113
113
|
*
|
|
114
114
|
* Defaults to {@link Cube.bucketSpecFn}.
|
|
115
115
|
*/
|
package/data/cube/ViewRowData.ts
CHANGED
|
@@ -19,9 +19,6 @@ 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
|
-
|
|
25
22
|
/**
|
|
26
23
|
* Label of the row. The dimension value or, for leaf rows. the underlying cubeId.
|
|
27
24
|
* Suitable for display, although apps will typically wish to customize leaf row rendering.
|